2015/12/03

CKEditor Image Upload On Codeigniter

這篇文章會一步步實作 CKEditorCodeIgniter 中圖片以及檔案上傳到 server 的功能,首先我們前往 CKEditor 官網下載最新的版本,這個 demo 使用的是 4.6,存檔後我會放在 assets/ckeditor/ 下,我會使用 Sybio/ImageWorkshop 這個套件對圖片做第一次的壓縮,我們通常不希望 user 存超大的東西在我們這啦,畢竟不是相簿,前台可以順利顯示為準則。

assets/ckeditor/config.js
CKEDITOR.editorConfig = function( config ) {
    config.toolbar =
        [
            {name:'document', items:['Source','-','Preview','Print']},
            {name:'clipboard', items:['Cut','Copy','Paste','PasteText','PasteFromWord','-','Undo','Redo']},
            {name:'basicstyles', items:['Bold','Italic','Underline','Strike','Subscript','Superscript','-','RemoveFormat']},
            '/',
            {name:'paragraph', items:['NumberedList','BulletedList','-','Outdent','Indent','-','Blockquote','-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock','-','BidiLtr','BidiRtl']},
            {name:'links', items:['Link','Unlink']},
            {name:'insert', items:['Image','Table']},
            {name:'tools', items:['Maximize']}
        ];
    config.enterMode = CKEDITOR.ENTER_BR;
    config.filebrowserImageUploadUrl = baseUrl + 'ckeditor/image_upload';
    config.filebrowserUploadUrl = baseUrl + 'ckeditor/file_upload';
};

這是我常用的 config,工具可以自行增減。

application/views/ckeditor.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CKEditor Upload Demo</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <script src="assets/ckeditor/ckeditor.js"></script>
    <script src="assets/ckeditor/adapters/jquery.js"></script>
    <script type="text/javascript" charset="utf-8">
    var baseUrl = "<?php echo base_url(); ?>";
    window.CKEDITOR_BASEPATH = "<?php echo base_url('assets/ckeditor'); ?>";

    $(function() {
        $('.ckeditor').ckeditor();
    });
    </script>
</head>
<body>
<?php echo form_open_multipart('ckeditor/upload'); ?>
<?php echo form_textarea(['name' => 'content', 'class' => 'ckeditor']); ?>
<?php echo form_submit('submit', '上傳'); ?>
<?php echo form_close(); ?>
</body>
</html>

簡單的設定一下 ckeditor 必要的環境。

application/controllers/Ckeditor.php
<?php

defined('BASEPATH') or exit('No direct script access allowed');

use PHPImageWorkshop\ImageWorkshop;

class Ckeditor extends CI_Controller
{
    /**
     * Upload path.
     *
     * @var string
     */
    private $path = 'assets/uploads/ckeditor/';

    /**
     * Index page.
     */
    public function index()
    {
        $this->load->helper('form');
        $this->load->helper('url');
        $this->load->view('ckeditor');
    }

    /**
     * CKEditor image upload
     *
     * Upload by ImageWorkshop package
     *
     * @return string
     */
    public function image_upload()
    {
        $this->load->helper('url');
        $funcNum = $this->input->get('CKEditorFuncNum');
        $source = $_FILES['upload']['tmp_name'];
        $file = $_FILES['upload']['name'];
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        $ratio = 1200; // 寬度最大比例
        $allows = [1, 2, 3]; // GIF, JPEG, PNG
        $newName = date('YmdHis').rand(1000, 9999); //新檔名
        $message = '';
        $url = '';

        try {
            $exif = exif_imagetype($source);

            if (!$exif) {
                throw new Exception('檔案格式錯誤');
            }

            if (!in_array($exif, $allows)) {
                throw new Exception('僅允許 gif, jpeg, png');
            }

            $imageInfo = getimagesize($source);
            $layer = ImageWorkshop::initFromPath($source);

            // 若寬度大於比例,縮放置比例
            if ($imageInfo[0] > $ratio) {
                $layer->resizeInPixel($ratio, null, true);
            }

            $fileName = "{$newName}.{$ext}";
            $layer->save($this->path, $fileName);
            $url = base_url($this->path.$fileName);
        } catch (Exception $e) {
            $message = $e->getMessage();
        }

        $string = "<script type=\"text/javascript\">window.parent.CKEDITOR.tools.callFunction({$funcNum}, '{$url}', '{$message}');</script>";

        $this->output
            ->set_content_type('text/html')
            ->set_output($string);
    }

    /**
     * CKEditor image upload
     *
     * @return string
     */
    public function file_upload()
    {
        $this->load->helper('url');
        $funcNum = $this->input->get('CKEditorFuncNum');
        $source = $_FILES['upload']['tmp_name'];
        $file = $_FILES['upload']['name'];
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        $newName = date('YmdHis').rand(1000, 9999); //新檔名
        $message = '';
        $url = '';

        try {
            $fileName = "{$newName}.{$ext}";
            $target = $this->path.$fileName;

            if (move_uploaded_file($source, $target)) {
                $url = base_url($target);
            } else {
                $message = '上傳失敗';
            }
        } catch (Exception $e) {
            $message = $e->getMessage();
        }

        $string = "<script type=\"text/javascript\">window.parent.CKEDITOR.tools.callFunction({$funcNum}, '{$url}', '{$message}');</script>";

        $this->output
            ->set_content_type('text/html')
            ->set_output($string);
    }
}

我們建立了 Ckeditor 的 controller,把 form 放在首頁,這個部份包含了圖片跟檔案上傳的程式碼,圖片部份也包含了檔案格式檢查、檔案尺寸檢查,如果還需要檢查更多的東西就在 try catch 裡面再加。

2015/10/19

watch.js

現在處於 node.js 幼幼班的階段,變覺得 node.js 的發明實在太方便,尤其是自己對 JavaScript 有一定的熟悉度所以學起來很愉悅,今天的 case 是,之前有介紹過 r.js,未來會朝向所有相關的 js 檔案全部壓成一個減少 server request,在開發階段會面臨一個問題就是東西有改動就要 compile,每次都要手動執行也挺累的,今天看到一個套件名為 watch,他可以偵測資料夾內檔案新增、修改、刪除的事件,於是寫了個 script 來幫我監聽並且做 auto compile。

~/watch/project.js

var watch = require('watch');
var path = require('path');
var exec = require('child_process').exec;
var monitorPath = '/var/www/project/assets/js/';

watch.createMonitor(monitorPath + 'src/', function(monitor) {
    monitor.on('changed', function(f) {
        var baseName = path.basename(f);
        var body = path.basename(f, '.js');
        var ext = path.extname(f);

        if (ext === '.js') {
            var command = 'r.js -o ' + monitorPath + 'build.js name=src/' + body + ' out=' + monitorPath + 'build/' + baseName + ' optimize=none';

            console.log('start compile ' + baseName);
            exec(command, function(error, stdout) {
                console.log(stdout);
            });
        }
    });
});

我在根目錄下面建立了一個 watch 的目錄,日後有新的專案的話就會多建一個檔案來監控,東西都設定完成後,只要在該目錄下 node project.js,watch 就會自動啟動了,我這邊編譯先把 uglifying 關掉,因為他會影響編譯速度,如果檔案已經確定是 final 不會變動的話,在去手動執行有 uglifying 的指令,如果在 windows 遇到錯誤訊息,只要把 command 改成 r.js.cmd 就可以了。

2015/10/13

r.js

用了一陣子 requirejs,之前都使用一些奇淫巧技讓每個頁面可以依照相依性導入各自頁面的 javascript,但其實這只達到了 requirejs 的非同步載入而已,檔案一多病沒有減少 request,requirejs 最強大的功能應該是他的 r.js 功能,他可以把相依性依照你設定的方式載入,並且合併成一個檔案,今天來示範一下我現在的布局。

目錄結構

- js/
 - build/
 - jquery/
 - src/
 - tool/

將所有的 javascript 檔案放在 js 目錄下,使用了四個目錄:

  • build/ (compiler 過後的檔案)
  • jquery/ (jQuery 相關檔案)
  • src/ (compiler 前的檔案)
  • tool/ (一些共用的工具檔案)

今天要示範我們要在 index.html 引入 index.js,index.js 要包含 jqueryjquery.formtools/func.js,並且依照相依性載入,首先我們建立 js/build.js

js/build.js
({
    baseUrl: './',
    paths: {
        'jquery': 'jquery/jquery.min',
        'form': 'jquery/jquery.form'
    },
    shim: {
        'form': ['jquery'],
        'src/index': ['form']
    },
    waitSeconds: 20
})

build.js 就是所有要 compiler 檔案的預設檔案,可以設定路徑、相依性等相關規範,我在這邊會將所有跟 jquery 相關的 plugin 都 depends on jquery,這樣當我各個 js 隨便調用任一個 jquery 的 plugin 時就會預載 jquery,接下來我們建立 src/index.js

src/index.js
requirejs(['tool/func'], function(func) {
    $(function() {
        func.init();
    });
});

我把 plugin 的調用寫在 build.js 裡面,工具的調用寫在各自的 js 裡面,這樣彈性變很大。

tool/func.js
define(function() {
    return {
        init: function() {
            console.log('func');
        }
    }
});

func.js 放個簡單的 funtion 讓 index.js 測試調用,接下來我們執行 r.js

$ r.js -o build.js name=src/index out=build/index.js

Tracing dependencies for: src/index
Uglifying file: D:/req/js/build/index.js

D:/req/js/build/index.js
----------------
D:/req/js/jquery/jquery.min.js
D:/req/js/jquery/jquery.form.js
D:/req/js/tool/func.js
D:/req/js/src/index.js

r.js 透過 build.js 執行了 src/index compiler 到 build/index.js,並且是依照我要的 order,接下來將結果放到 index.html 裡面。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>r.js</title>
    <script data-main="js/build/index" src="js/require.js"></script>
</head>
<body>

</body>
</html>

執行網頁後在 console 得到 func 的 output,如果東西在開發階段,可以加一個參數讓 compiler 的檔案不作最佳化, $ r.js -o build.js name=src/index out=build/index.js optimize=none,這樣就可以方便除錯了。

假設我們今天編譯了一個公用的工具,一旦你有修改到那隻工具,就要面臨每個檔案都必須重新編譯的問題,我們可以另外設一個 config 檔案來做群組的編譯。

builds.js
({
    paths: {
        'jquery': '../jquery/jquery.min',
        'form': '../jquery/jquery.form',
        'init': '../tool/func.js',
    },
    shim: {
        'form': ['jquery'],
        'src/index': ['form']
    },
    baseUrl: './',
    appDir: 'src/',
    dir: 'build/',
    waitSeconds: 20,
    modules: [
        {name: 'index'},
        {name: 'other'}
    ]
})

設定完畢後執行 r.js -o builds.js,就會將 modules 裡面的內容全部重新跑一遍。

2015/10/07

CodeIgniter Migration

一般的程式設計師設計資料庫常常會使用 PHPMyAdmin 或者是一些軟體直接在 DB 上做表單的建立或管理,一個人的專案可能還 ok,但如果遇到 co-work,或者是測試機與正式機不同機器,很難避免粗心漏掉內容的問題,最好的解決方案就是用程式產生相關的內容,PHP 兩個熱門的框架 Laravel 以及 CodeIgniter 都有提供 Database Migration 的功能,今天來初探 CodeIgniter 的 Migration 部分。

CodeIgniter 所有 Migration 的內容只要看過上面兩個連結就可以做完,讓我們來 step by step 案例。

前置作業

application/config/migration.php

這個檔案是設定 migration config 的位置,有幾個重點要設定

$config['migration_enabled'] = true;

miagration_enabled 的部分開啟。

$config['migration_type'] = 'sequential';

修改 migration_typesequential,看個人習慣,這個設定是指 migration 的檔案取名方式,如果是 timestamp 檔名取法則用日期當做順序,例: 20121031104401_add_blog.php,而 sequential 的話就是按照檔名排序,例:001_add_blog.php,其他設定如 miagration 資料夾位置,這邊我用 default,所以我們建立一個目錄 application/migrations/

實做

application/migrations/001_create_users_table.php
class Migration_Create_users_table extends CI_Migration
{
    public function up()
    {
        $this->dbforge->add_field([
            'id' => [
                'type' => 'int',
                'unsigned' => true,
                'auto_increment' => true,
            ],
            'username' => [
                'type' => 'varchar',
                'constraint' => 50,
            ],
            'password' => [
                'type' => 'varchar',
                'constraint' => 50,
            ],
            'gender' => [
                'type' => 'tinyint',
                'constraint' => 1
            ],
            'created_at' => [
                'type' => 'datetime'
            ],
            'updated_at TIMESTAMP DEFAULT NOW() ON UPDATE NOW()'
        ]);

        $this->dbforge->add_key('id', true);
        $this->dbforge->create_table('users');
    }

    public function down()
    {
        $this->dbforge->drop_table('users');
    }
}

class name 一定要是 Migration_ 加上字首大寫並且去掉序列的檔名,不然不會動,up 是建立的部分,down 則是給 rollback 用的,所以我們這邊的 up 是 create 某個 table 的話,down 就是 drop 某個 table,以此類推,add_key 的部分有加 true 會是 primary key,沒有的話就是一般的 index,基本上欄位的設定就是把 sql 的欄位建立方式填進去並且給值,constraint 不給的話預設會是最大,如果在 MySQL 裡面試是非題的話就是用 true false 給值,像是 null => false 則預設不能 null,如果沒下的話預設值看 MySQL 怎麼設定。

application/controllers/Migrate.php
class Migrate extends CI_Controller
{
    public function start()
    {
        $this->load->library('migration');

        if ($this->migration->latest() === false) {
            echo $this->migration->error_string();
        } else {
            echo 'migrated'.PHP_EOL;
        }
    }
}

我們建立一個 controller 來執行 migration,網路上有很多方法可以讓這個 controller 只能透過 cli 執行,但我實際跑過,如果你是下 $this->migration->latest() 的話,從 browser 重複執行並不會影響建構好的 table,所以我是沒有打算放,而且客戶常常都只有給 FTP 哪有辦法走 cli 模式。

建立完畢後我在本機的 cli 跑 php index.php migrate start 得到:

"chan" Sid: S-1-5-21-759891990-2490853520-3465201229-1000
migrated

打開資料庫看得到了 migrations 以及 users table,這樣就是順利完成了,接下來我們來模擬幾個場景。

application/migrations/002_add_ip_to_users_table.php
class Migration_Add_ip_to_users_table extends CI_Migration
{
    public function up()
    {
        $field = [
            'ip' => [
                'type' => 'varchar',
                'constraint' => 100,
                'null' => false,
            ]
        ];

        $this->dbforge->add_column('users', $field, 'password');
    }

    public function down()
    {
        $this->dbforge->drop_column('users', 'ip');
    }
}

這個場景就是在 users 裡面增加一個欄位,add_column 的第三個欄位等於 MySQL 的 after

application/migrations/003_drop_gender_from_users_table
class Migration_Drop_gender_from_users_table extends CI_Migration
{
    public function up()
    {
        $this->dbforge->drop_column('users', 'gender');
    }
}

users table 移掉 gender 這個 column。

004_modify_password_from_users_table.php
class Migration_Modify_password_from_users_table extends CI_Migration
{
    public function up()
    {
        $field = [
            'password' => [
                'name' => 'password',
                'type' => 'varchar',
                'constraint' => 100,
            ]
        ];

        $this->dbforge->modify_column('users', $field);
    }
}

password 的長度增加到 100,在 MySQL 如果要改一個欄位的內容 alter 的語法你必須把其他欄位設定都在打一次,所以不要只有打 'constraint' => 100 而已,這樣會出事。

2015/10/06

CodeIgniter Drivers

Codeigniter 裡面有一個功能叫 drivers,但官方的文件著墨很少,網路上討論的文章也不算多,但我覺得以一個不是使用 namespace 那麼方便做 DI 以及調用資源的 framework 來說,這個功能還頗好應用的,一般人使用 CI 都會用他的 library 功能,把自己的 class 放在這邊調用,但大部分就是一層式的沒有階級觀念,就不用提 interface 了,dirvers 可以靈活的做到調用,先來講怎麼設定 driver 環境,以各家第三方登入為例。

首先我們先建立目錄

  • Auth
  • Auth/Auth.php
  • Auth/drivers
  • Auth/drivers/Auth_facebook.php
  • Auth/drivers/Auth_google.php

命名規則就是檔名必須跟目錄名稱相同,第一個字首要大寫(包含初始的目錄),只要任何一個名稱取錯都不會動,我試過了。

Auth.php
class Auth extends CI_Driver_Library
{
    protected $valid_drivers;

    public function __construct()
    {
        $this->valid_drivers = array('facebook', 'google');
    }

    public function login()
    {
        echo 'login from auth driver'.PHP_EOL;
    }
}

$valid_drivers 是必要參數,他表示允許調用的 dirver 名稱,呼叫的時候不需要帶 parent name,這樣寫起來也比較清爽。

Auth_facebook.php
class Auth_facebook extends CI_Driver
{
    public function login()
    {
        echo 'login from auth facebook'.PHP_EOL;
    }
}

基本上被調用的 driver 子項目很簡單,繼承 CI_Driver 就可以了,我們接著建立一個叫 Login 的 controller。

application/controllers/Login.php
class Login extends CI_Controller
{
    public function index()
    {
        $this->load->driver('auth');
        $this->auth->login();
        $this->auth->facebook->login();
    }
}

在 command line 執行 php index.php login index 會得到

login from auth driver
login from auth facebook

寫到這就知道前面部屬好的話,之後要調用各家的 login 只要在 Auth/drivers 下增加,並且加入 $valid_drivers 設定即可,根據 SOLID 原則,會變動的 config 內容不應該存在在 class 裡面,所以我們在 config 增加一個 auth.php

application/config/auth.php
$config['valid_drivers'] = array('facebook', 'google');

然後將 Auth.php 改寫如下。

class Auth extends CI_Driver_Library
{
    protected $valid_drivers;
    protected $ci;

    public function __construct()
    {
        $this->ci =& get_instance();
        $this->ci->config->load('auth', true);
        $this->valid_drivers = $this->ci->config->item('valid_drivers', 'auth');
    }

    public function login()
    {
        echo 'login from auth driver'.PHP_EOL;
    }
}

這樣的話之後要加內容只要改 config 就好,但這樣的彈性還不是最好的,我們應該要讓 Login controller 可以接受變數選擇調用哪個登入才對,來對他動一點手腳。

class Login extends CI_Controller
{
    public function index($method = null)
    {
        $this->load->driver('auth');
        $this->auth->login();
        $this->auth->{$method}->login();
    }
}

然後執行 php index.php login index facebook,這樣會得到跟上面一樣的結果,但我們應該更嚴謹一點,檢查該 method 是否存在,在來強化一下。

class Login extends CI_Controller
{
    public function index($method = null)
    {
        $this->config->load('auth', true);

        if (!in_array($method, $this->config->item('valid_drivers', 'auth'))) {
            die('method not exists');
        }

        $this->load->driver('auth');
        $this->auth->login();
        $this->auth->{$method}->login();
    }
}

如果亂執行一通 php index.php login index twitter,會得到 method not exists,這大致上是 drivers 的全貌,怎麼變化應用讓功能更有彈性就要視不同的情況去配置了。

2015/09/23

PHP Mockery

去上了鐵哥的 PHP 測試的課程,回來一股腦的想要把每個看的到的可怕東西都 refactor,但鐵哥有交代,refactor 之前最好還是先下測試,正所謂先套保命索,再去走鋼索,好詩好詩,於是這幾天我除了重複看鐵哥的 readme 想熟記心法以外,完全醉心於 Mockery 了,之前也寫過一陣子測試,但有些情況很難模擬,部分原因是自己 code 拆的不夠細不好測,不然就是某些情況必須有外部的結果才能測試,例如說 mail 啦、資料庫等等,鐵哥介紹 Mockery 這個好用的工具以後解決了我的問題,而且我發現寫 code 為了想要好 mock 來拆 code 變成一種 refactoring 的依據,筆記一下這幾天的心得,用最簡單的方式解說,中間跳過一些繁複的過程。

DI

Game.php
class Game
{
    public function result(DB $db)
    {
        return $db->data();
    }
}
DB.php
class DB
{
    public function data()
    {
        return false;
    }
}

上面執行 var_dump((new Game)->result(new DB)); 的結果會是 false,我們來撰寫測試。

GameTest.php
use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $db = new DB;
        $game = new Game();

        $this->assertTrue($game->result($db));
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

F

Time: 519 ms, Memory: 9.75Mb

There was 1 failure:

1) GameTest::testResult
Failed asserting that false is true.

D:\www\phpunit\tests\GameTest.php:12

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

因為 DBdata() 回傳的是 false,讓我們來 mock 他。

use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $db = m::mock('DB');
        $db->shouldReceive('data')
            ->once()
            ->andReturn(true);
        $game = new Game();

        $this->assertTrue($game->result($db));
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

.

Time: 485 ms, Memory: 10.25Mb

OK (1 test, 1 assertion)

Mock 內部呼叫 method

Game.php
class Game
{
    public function result()
    {
        return $this->data();
    }

    public function data()
    {
        return false;
    }
}

執行 var_dump((new Game)->result()); 的結果為 false

GameTest.php
use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = new Game();

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

F

Time: 415 ms, Memory: 9.75Mb

There was 1 failure:

1) GameTest::testResult
Failed asserting that false is true.

D:\www\phpunit\tests\GameTest.php:11

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

讓我們使用 partial 來 mock 他。

use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = m::mock('Game[data]');
        $game->shouldReceive('data')
            ->once()
            ->andReturn(true);

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

.

Time: 442 ms, Memory: 10.25Mb

OK (1 test, 1 assertion)

Mock new class

Game.php
class Game
{
    public function result()
    {
        $db = new DB;

        return $db->data();
    }
}

執行 var_dump((new Game)->result()); 結果為 false

GameTest.php
use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = new Game;

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

F

Time: 403 ms, Memory: 9.75Mb

There was 1 failure:

1) GameTest::testResult
Failed asserting that false is true.

D:\www\phpunit\tests\GameTest.php:11

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

讓我們用 overload 來 mock 他。

use Mockery as m;

class GameTest extends PHPUnit_Framework_TestCase
{
    public function testResult()
    {
        $game = new Game;
        $mock = m::mock('overload:DB');
        $mock->shouldReceive('data')
            ->once()
            ->andReturn(true);

        $this->assertTrue($game->result());
    }

    public function tearDown()
    {
        m::close();
    }
}
執行結果
PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

.

Time: 466 ms, Memory: 10.25Mb

OK (1 test, 1 assertion)

overload 目前發現對 static method 也有效用,但他會無法偵測到其他事件,例如 once()twice(),所以 static function 建議抽出來執行。

class Game
{
    public function result()
    {
        $data = $this->data();

        return $data;
    }

    public function data()
    {
        return DB::data();
    }
}

這樣就可以使用剛介紹 partial 的方法驗證了。

2015/09/16

jQuery Map And Get

我們常常會用到 jQuery 去 fetch 一段 DOM 以後拿到值來做事情,舉例來說今天有三個 input。

HTML
<input type="text" value="a">
<input type="text" value="b">
<input type="text" value="c">

我們想要 fetch DOM 把所有的 value 拿出來並且組成 a, b, c 這樣的字串該怎麼做,方法當然很多,一般來說會利用 $.each 來組。

$.each
var result = [];

$(':input').each(function() {
    result.push(this.value);   
});

result = result.join(',');

console.log(result); // a,b,c

如果理解更多的 jQuery API 的話,可以用一些特殊技巧用更少的行數達成某些結果。

$.map
var result = $(':input').map(function() {
    return this.value;   
}).get().join(',');

console.log(result); // a,b,c

mapget 是 jQuery 常拿來處理陣列的技巧,如果善加利用可以省去很多迴圈或是宣告的事情。

2015/08/24

Detect Built-in Browser

現在有很多原生 App 會開啟 built-in browser,像是 Facebook、 Line,往往 mobile version 的網頁在 app built-in browser 不會 work,上個專案客戶就希望如果展開軟體的是 built-in browser 的話出現希望 user 打開一般 browser 的提示,好在 built-in borwser 有提供相關的 userAgent,抓取方式如下。

var nu = navigator.userAgent;

// Line
if (nu.match(/safari line/i) !== null) {
    // Do something
}

// Facebook
if (nu.match(/facebook/i) !== null) {
    // Do something
}

2015/08/08

18.媽媽的肩膀

媽媽的肩膀最牢靠了。

我是小恐龍~吼~

我沒有很愛當小瓢蟲~

2015/08/04

User And Group

user and group 主要是設定使用者對資料夾存取的權限,以我的狀況比較少碰到這個設定,memo 一下幾個我覺得比較重要的指令。

User

useradd 新增帳號

  • -d 設定 home 目錄
  • -g 預設群組
  • -G 次群組
  • -m 自動建立 home
  • -M 不自動建立 home
  • -s bash 位置
# 建立帳號 chan
useradd chan
 
# 建立帳號不設定 home 資料夾並設定多組次群組
useradd chan -M -G group1,group2 -s /bin/bash
 
# 建立帳號並指定登入資料夾
useradd chan -d /var/www/test
 
# 幫帳號設定密碼
passwd chan

usermod 修改帳號

  • -d 設定 home 目錄
  • -g 預設群組
  • -G 次群組
  • -m 自動建立 home
  • -M 不自動建立 home
  • -s bash 位置
  • -L lock user
  • -U unlock user

基本上 usermoduseradd 指令不會差太多,主要是用來修改一些當參數。

# 鎖住帳號
usermod -L chan
 
# 解鎖帳號
usermod -U chan
 
# 取代次群組
usermod -G group3 chan
 
# 增加次群組
usermod -aG group3 chan

userdel 刪除帳號

# 刪除帳號
userdel chan

# 刪除用戶的同時,一併把使用者的家目錄及本地郵件存儲的目錄或檔也一同刪除
userdel -r chan

group

groupadd 建立群組

# 建立群組
groupadd group1

gpasswd 設群組成員和密碼

  • -a 增加群組成員
  • -d 刪除群組成員
# 增加使用者到群組
gpasswd -a chan group1
 
# 移除使用者到群組
gpasswd -d chan group1

groupmod 修改群組

  • -g 修改 gid
  • -n 修改群組名稱
# 修改群組名稱
groupmod -n newname oldname

目錄

chmod 修改目錄權限

  • -R 遞迴式修改

Linux 檔案的基本權限就有九個,分別是 owner/group/others 三種身份各有自己的 read/write/execute 權限,檔案的權限字元為:-rwxrwxrwx,r:4 w:2 x:1,每種身份 (owner/group/others) 各自的三個權限 (r/w/x) 分數是需要累加的,例如當權限為: [-rwxrwx—] 分數則是:

  • owner = rwx = 4+2+1 = 7
  • group = rwx = 4+2+1 = 7
  • others= — = 0+0+0 = 0
# 修改檔案或目錄權限
chmod -R 777 DIRECTORY

chgrp 改變所屬群組

  • -R 遞迴式修改
# 修改擁有群組
chgrp -R group1 DIRECTORY

chown 改變檔案擁有者

  • -R 遞迴式修改
# 改變擁有者
chown -R chan DIRECTORY
 
# 改變擁有者加擁有群組
chown -R chan:group1 DIRECTORY

其他

# 查詢 server 有多少使用者
cat /etc/passwd
 
# 由於 server 使用者太多了,比較建議查 /etc/shadow
# 我們設定的帳號都在末端,有密碼的跟沒密碼的一目了然
cat /etc/shadow
tail -10 /etc/shadow
 
# 查詢現在有哪些群組
cat /etc/group
tail -1- /etc/group
getent group
 
# 查詢單一群組有多少 user
getent group group1
 
# 查詢 user 加入了多少群組
groups chan

# 查詢目前登入者 uid gid groups
id

# 查詢某個帳號
id sshd
uid=113(sshd) gid=65534(nogroup) groups=65534(nogroup)

# 查詢某個帳號 uid
id -u sshd
113

# 查詢某個帳號 gid
id -g sshd
65534

# 用 uid 反查帳號
id -un 113
sshd

# 用 gid 反查群組
id -gn 65534
nogroup

2015/08/01

lftp

自己在 DigitalOcean 租了空間,所以我現在習慣把專案上傳到 server,在 server 編輯跟上 git server,只要我有 terminal 的情況下在哪邊都可以工作 QQ,如果客戶的 demo 網站在我這邊的話就很方便,但常常會遇到的情況是客戶有自己的 ftp,所以變成邊改要邊上傳,在本機的 windows 環境下我使用 WinSCP 來作這件事情,他可以 monitor 某個資料夾,監控到有檔案更新便自動上傳 FTP,並且可以設定一些 filter,在 Linux 部分我一直在找最佳解,用了幾個方法以後 lftp 可能是最接近的答案了。

傳統的 ftp 指令必須掛在 server 上,要上傳或下載要自己切換根本不可能,假設不能自動檢查更新,起碼要可以增量備份,於是我用了 rsync 跟 scp 這兩個指令多次測試,都沒有辦法順利完成我要的,lftp 好處是,他可以模擬 http 的辦法進入 ftp,所以你可以打好一堆指令一次執行,也可以把執行腳本寫在文字檔裡去吃,重要的是他有 mirror 功能可以做到增量修改備份上傳這件事,除了自動監控以外沒什麼可以挑剔的了。

簡易參數介紹

  • -c 後面直接加上所需要的指令
  • -e | –delete 沒有檔案就刪除
  • –only-newer 上傳或下載比較新的
  • –parallel 同時使用的序列
  • –verbose 詳盡模式
  • -r | –no-recursion 不遞迴檢查
  • -R 下載變上傳,備分到遠端
  • -u 後面則是接上帳號與密碼,就能夠連接上遠端主機了,如果沒有加帳號密碼, lftp 預設會使用 anonymous 嘗試匿名登入
  • -f 可以將指令寫入腳本中,這樣可以幫助進行 shell script 的自動處理喔!

你可以直接打整行的命令

lftp -e "put /local/path/xxx.mp4 bye" -u username,password ftp.myftp.com

也可以將指令寫在指令中,下面是我自己的工作習慣設訂

upload.lftp
open username:password@ftp.myftp.com # 若要連 sftp 可改成 sftp://username:password@myftp.com
cd /to/path/ # 遠端需要的正確資料夾,如果沒有可以不用
mirror -R /local/path/ --only-newer --verbose –parallel=10 --exclude .git/ --exclude upload.lftp --exclude-glob *.swp
echo done!

這樣只要執行下面的指令就可以上傳了

lftp -f upload.lftp

mirror 介紹

mirror 這個指令可以做到上下傳同步的功能,也可以指定資料夾同步,主要的下法是 mirror source target,假設今天 remote 的目錄是 /www/public/ local 是 /var/www/site/,要下載同步指令為 mirror /www/public/ /var/www/site/,上傳同步則相反,mirror -R /var/www/site/ /www/publc/,假設我們今天是要同步下載 css 資料夾下的檔案,指令為 mirror /www/public/css/ /var/www/site/css/,如果是要同步上傳的話就是 mirror -R /var/www/site/css /www/public/css,如果你確定你是在該資料夾的決對路徑,假設你已經在 /var/www/site/ 下了,你要同步下載的話只要指定 source 路徑即可,mirror /www/public/css,如果你要同步上傳,反過來就是對方路徑是 target,所以你在 config 必須先指定到該資料夾成為 target,然後你在指定自己 source 要同步的目錄,不打的話就是整個目錄。

open username:password@ftp.com
cd /www/public/
mirror -R --verbose css/

vim 的 user 可以裝上 lftp-sync.vim 這個由 c9s 大大寫的套件,可以存檔後上傳所有的 buffer 或者是單檔,然後在寫一個 sync 的檔案拿來跑一些可能是由美術更新的資料夾,每次要做專案前 sync 一次,應該就可以大致解決檔案同步問題。

###參考網站###

2015/07/21

15.我滿月了

在這個早晨開始,我正式出生一個月囉。

躺在媽咪的懷抱等大家來看我。

Check it out yo, 布丁 in da house。

我馬麻是最棒的馬麻。

Check check check, DJ 布丁 in da house!

但其實我只是想 take a break。

多謝各位的紅包,但我先睡啦~

2015/07/18

14.大家互相

媽媽常常抱我,偶爾我也抱一下媽媽。

把拔有好兄弟所以我有帝寶睡。

2015/07/17

13.祖孫倆不厭

阿祖你好。

拔獅子的毛要等他睡著。

2015/07/12

常用工具筆記

記錄一下自己幾乎每天都會用到的工具,這樣在轉移工作環境時安裝環境時也比較快速。

文字編輯

VIM

VIM 是我的謀生工具,撰寫程式全靠他。

notepad++

一般文字的處理我會使用 notepad++,隨時記錄事情,notepad++ 有一個很方便的地方是他可以將你貼上的內容暫存,所以你電腦無預警重開的話資料都還在。

資料庫管理

HeidiSQL

目前使用過最強大的資料庫管理 GUI 非 Navicat 莫屬,但他要錢,還不便宜,我很想買一套,不過真的太貴了。

SSH

Xshell

非常強大且免費的免費軟體,我也使用他來連結 ptt,我也會同時安裝 Xftp 來存取 ssh 的檔案,lrzsz 沒有這個方便,不過他不是我的主力 FTP 軟體。

檔案管理

FreeCommander

用過類似 TotalCommander 工具的人根本不太會想用 Windows 內建的檔案管理工具,FreeCommander 對我來說是各方面都不需要設定的簡單版 TotalCommander,幾乎是裝好不用改什麼就符合我的需求。

秀圖軟體

XnView

選擇這套軟體主要是因為安裝檔案小,開啟速度快,該有的編輯功能也都有。

FTP/SFTP

WinSCP

這套軟體的有幾個功能使用上很方便,快速篩選檔案、同步流覽(他的同步流覽強在如果另一邊沒有相等的資料夾會問你要不要建立目錄,其他的 FTP 通常都是自動解除同步流覽)、自動偵測更新檔案上傳(這是我當初選用他的最大原因,不過感覺他對必須上傳檔案的判斷有些問題,希望越改越好)。

Git

msysgit

即便公司不是使用 Git 當作 repository base 也沒關係,因為他的 terminal 介面也很好用,用打的比用滑鼠點的快多了。

遠端管理

TeamViewer

這套強大的遠端管理工具應該不用我多做介紹了。

以上的軟體絕大部分都可以在 File Hippo 找到,我也會順裝他的 Manager,保持我的軟體更新,上面幾個軟體都有一樣的特點,免費、輕快,大部分都有偵測自動更新,這樣的軟體最合我胃口了,用的喜歡的各位記得給原作者 donate 喔。

2015/07/07

12.本大爺洗香香

要下水囉,驚驚。

要好好對我喔。

還是趴著舒服。

終於洗好了,開始擦身體。

衣服一件一件穿回來。

好蘇胡。

2015/07/05

11.我有名字了

大家好,我有名字囉,請大家叫我小東穎~

今天穿的是可愛粉紅小熊衣。

信我者快樂一輩子,哈哈。

2015/07/03

PHP-CS-Fixer On Vim

PHP 之前為人所詬病的地方就是沒有統一的撰寫風格以及結構,MVC 觀念興起後,PHP 也出了不少框架,在架構方面得到了很大的改善,但 coding style 還是存在很大的問題,假設 A 公司跟 B 公司都有自己的 convention,那還是沒有所謂的 standard 存在,所以有一個團體制定了一個標準 PHP-FIG,裡面有各種程式碼的撰寫規範,當然這是一種形式上的規範,你不照他那樣寫 PHP 也是會動,只是如果有在使用 github 套件,以及使用 MVC framework 的人,會發現知名熱門的框架幾乎都往這邊靠了,所以如果花點時間去看他的規範參考,之後你接觸其他人的東西速度會快很多。

今天要介紹的不是規範的內容,有慶去的可以去看 Apple 大的投影片,他已經把比較多重要的內容放上來了,今天我要介紹的是工具,PHP-CS-Fixer,這個工具是可以幫你整理現行的 php code,假設你公司已經有許多歷史包袱,但你很想把你看到的內容都整理成 psr-0psr-2,只要執行這個工具就可以幫你完成,我們來寫一下超不符合規定的 code。

psr.php
$name = 'Chan'; 

if($name !== '')
    echo "hi, my name is {$name}" . PHP_EOL;

for($i = 1; $i <=10; $i++){
    echo $i . PHP_EOL;
}

function test() {

}

上面的程式碼有幾個不符合規定的項目:

  • if 的寫法不可以省略 {}
  • 歪七扭八的 format 如 for( 應該要 for (
  • 結尾有多餘的 trailing white space
  • function{} 要換行

安裝好了 PHP-CS-Fixer 之後,打開 terminal 並跑去我們剛檔案的位置,執行下面的指令:

php-cs-fixer.phar fix psr.php
結果
$name = 'Chan';

if ($name !== '') {
    echo "hi, my name is {$name}".PHP_EOL;
}

for ($i = 1; $i <= 10; ++$i) {
    echo $i.PHP_EOL;
}

function test()
{
}

php-cs-fixer 預設值是跑 symphony 的結構,會改變一些東西,如 空格.空格 會變成 .for ($i = 1; $i <= 10; $i++) 會變成 for ($i = 1; $i <= 10; ++$i),但完全不會影響現行程式的運行,或許你會不喜歡,那可以加上參數 --level=psr2,這樣的結果就會做出只針對 psr2 的修正。

php-cs-fixer.phar fix psr.php --level=psr2
加上 --level=psr2 結果
$name = 'Chan';

if ($name !== '') {
    echo "hi, my name is {$name}" . PHP_EOL;
}

for ($i = 1; $i <=10; $i++) {
    echo $i . PHP_EOL;
}

function test()
{
}

不過你可以看到 psr-2 並沒有幫你把 $i <=10 變成 $i <= 10 的格式,所以可以憑自己喜好選擇,而 vim 有 vim-php-cs-fixer 可以直接支援在 vim 裡面直接使用 php-cs-fixer,安裝以後執行 <leader>pcf 表示修正目前檔案,<leader>pcd則是修正目前檔案所屬資料夾下所有的檔案,相當方便。

2015/07/02

10.其實我很害羞

我在媽媽肚子裡照超音波的時候就喜歡遮臉,大家以為我很害羞。

其實我是陽光男孩啦 cc。

2015/07/01

09.跟你想的不一樣

很多人以為小寶寶睡覺是這樣。

但其實是這樣。

等我睡飽飽就變這樣了,哇哈哈。

2015/06/30

08.我喜歡被抱

我自己躺的時候就哭哭,然後媽媽把我抱起來我就偷笑了。

2015/06/29

2015/06/28

06.我是小超人

世界就靠我保護了。

陽光照耀我眼睛。

2015/06/26

PHPUnit

單元測試對程式設計來說是蠻重要的ㄧ件事情,測試寫久了對你實際程式碼撰寫思路也有一定的幫助,簡單介紹一下 PHPUnit 的使用方式。

首先先去官網將 phpunit.phar 裝起來,並確定在 terminal 可以呼叫,在你的 project 裡面使用 composer 將 phpunit/phpunit require 進來,我使用 composer autoload psr-4 功能做 class 的 autoloading,並且將預設的 class 放在 src/ 目錄下,所以目前 composer.json 會長這樣。

{
    "require": {
        "phpunit/phpunit": "^4.7"
    },
    "autoload": {
        "psr-4": {
            "": "src/"
        }
    }
}

執行 composer -o dump-autoload 確保所有的設定 ok,我們來建一個要測試用的檔案,我在 src/ 目錄下建了一個 Chan.php,內容如下。

src/Chan.php
class Chan
{
    public function myName()
    {
        return 'Chan';
    }
}

接下來要建立我們的測試檔案,建立測試檔案命名有一定的規範,假設我是要測試 Chan.php,那我要建立 tests\ChanTest.php,要測試某個 function 的話,function name 要這樣取 public function testMyName,PHPUnit 僅會跑開頭為 test 的 function。

tests/ChanTest.php
class ChanTest extends PHPUnit_Framework_TestCase
{
    public $chan;

    public function __construct()
    {
        $this->chan = new Chan;
    }

    public function testMyName()
    {
        $this->assertEquals('Chan', $this->chan->myName());
    }
}

接下來我們在 terminal 執行 phpunit script

phpunit --bootstrap vendor/autoload.php tests/

--bootstrap 會先跑我們希望他預跑程式,有可能是一些設定之類的,這邊我指向 autoload 的部分,跑完結果是。

PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

.

Time: 375 ms, Memory: 9.25Mb

OK (1 test, 1 assertion)

這表示完全正確,我們故意修改一下 function

public function testMyName()
{
    $this->assertEquals('Phoebe', $this->chan->myName());
}

讓我們在執行一次 PHPUnit script,這次得到

PHPUnit 4.7.5 by Sebastian Bergmann and contributors.

F

Time: 378 ms, Memory: 9.25Mb

There was 1 failure:

1) ChanTest::testMyName
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'Phoebe'
+'Chan'

D:\www\test\tests\ChanTest.php:14

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

PHPUnit 明確的把錯誤的訊息指出來給你,所以修正起來會很方便,我們是否每次檢查都要打這麼多字,當然不用,我們只要預先把 config 寫到 phpunit.xml 裡就可以了,官網的說明在此,依照我們的情況的基礎內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">src/</directory>
        </whitelist>
    </filter>
</phpunit>

當然 phpunit.xml 還有非常多的東西可以設定,可以參考一下官方說明取用,測試結果有很多 function 可以用,最常用的應該就是 assertEquals 以及 assertTrue(),其他的情況可以參考這邊搭配使用。

05.我不喜歡換尿布

每次幫我換尿布時我就是要哭。

換完以後一秒鐘就安靜。

媽媽在幫我練習拍嗝。

我今天心情很不錯。

2015/06/25

04.我是獅子王

這是我在月子中心坐騎旁邊的圖案,我以後是獅子王。

我雖然很可愛,可是吃母奶的時候我吃一下就睡著,吃配方奶我就猛吸,壞壞。