Laravel 小技巧

本文节选自50分钟学会Laravel 50个小技巧,有修改。针对Laravel 5.1 版本。

一共分为8篇

Model 篇

  1. model自动检验字段合法性

     class User extends Eloquent
     {
         public static $autoValidate = true;
         protected static $rules = [
             'email' => 'required|between:6,255|email|unique:backend_users',
             'login' => 'required|between:2,255|unique:backend_users',
             'password' => 'required:create|between:4,255|confirmed',
             'password_confirmation' => 'required_with:password|between:4,255'
         ];
        
         protected static function boot()
         {
             parent::boot();
             // You can also replace this with static::creating or static::updating
             static::saving(function ($model) {
                 if($model::$autoValidate) {
        
                     return $model->validate();
                 }
             });
         }
        
         public function validate() { }
     }
    
  2. 用trait创建通用方法

     trait CrudTrait
     {
         /**
          * 创建实例
          *
          * @author kelvinblood <admin@kelu.org>
          * @param array $array
          * @return mixed
          */
         public static function createInstance(Array $array)
         {
             $className = get_called_class();
             $newClass = new $className();
             foreach ($array as $key => $value) {
                 $newClass->$key = $value;
             }
             $newClass->save();
    
             return $className::find($newClass->uuid);
         }
     }
    
  3. uuid作为主键

      use Ramsey\Uuid\Uuid;
         
      trait UUIDModel
      {
          public $incrementing = false;
          protected static function boot()
          {
              parent::boot();
              static::creating(function ($model) {
                  $key = $model->getKeyName();
                  if(empty($model->{$key})) {
                      $model->{$key} = (string)$model->generateNewId();
                  }
              });
          }
    
          public function generateNewUuid()
          {
              return Uuid::uuid4();
          }
      }
    
  4. 简单的加减数字

    这样就可以省去读取、赋值和 save 操作了。

     $customer = Customer::find($customer_id);
     $loyalty_points = $customer->loyalty_points + 50;
     $customer->update(['loyalty_points' => $loyalty_points]);
        
     // adds one loyalty point
        
     Customer::find($customer_id)->increment('loyalty_points', 50);
     // subtracts one loyalty point
        
     Customer::find($customer_id)->decrement('loyalty_points', 50);
    
  5. 为model添加多余字段

    嗯,这个方法算是取巧,也很实用。

     function getFullNameAttribute()
     {
         return $this->first_name . ' ' . $this->last_name;
     }
    
  6. 保存model时返回关系

     public function store()
     {
         $post = new Post;
         $post->fill(Input::all());
         $post->user_id = Auth::user()->user_id;
         $post->user;
         return $post->save();
      }
    
  7. 同时新建model和migration.

      php artisan make:model Books -m
    

Eloquent ORM 篇

  1. 有条件的关联关系

     class myModel extents Model
     {
          public function category()
          {
          return $this->belongsTo('myCategoryModel', 'categories_id')->where('users_id', Auth::user()->id);
          }
     }
    
  2. 可排序的关联关系

     class Category extends Model
      {
          public function products()
          {
              return $this->hasMany('App\Product')->orderBy('name');
          }
      }        
    
  3. 仅显示关联关系存在的数据

      class Category extends Model
      {
          public function products()
          {
              return $this->hasMany('App\Product');
          }
      }
         
      public function getIndex()
      {
          $categories = Category::with('products')->has('products')->get();
          return view('categories.index', compact('categories'));
      }
    
  4. Where的语法表达式

    真是够奇葩的,呵呵ε=(・д・`*)ハァ…

     $products = Product::where('category', '=', 3)->get();
     $products = Product::where('category', 3)->get();
     $products = Product::whereCategory(3)->get();        
    
  5. Query Builder 的 havingRaw 方法

     SELECT *, COUNT(*) FROM products GROUP BY category_id HAVING count(*) > 1;
        
     DB::table('products')
         ->select('*', DB::raw('COUNT(*) as products_count'))
         ->groupBy('category_id')
         ->having('products_count', '>', 1)
         ->get();
     Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();
    
  6. 日期筛选

     $q->whereDate('created_at', date('Y-m-d'));
     $q->whereDay('created_at', date('d'));
     $q->whereMonth('created_at', date('m'));
     $q->whereYear('created_at', date('Y'));
    
  7. 随机取数据

     $questions = Question::orderByRaw('RAND()')->take(10)->get();        
    
  8. 只取特定字段lists

    $employees = Employee::where('branch_id', 9)->get()->lists('full_name', 'id');
    

Blade 篇

  1. 获取数组首尾的元素

     //hide all but the first item
     @foreach ($menu as $item)
     <div @if ($item != reset($menu)) class="hidden" @endif>
     <h2>\{\{ $item->title }}</h2>
          </div>
     @endforeach
    
      //apply css to last item only
      @foreach ($menu as $item)
     <div @if ($item == end($menu)) class="no_margin" @endif> 
     <h2>\{\{ $item->title }}</h2>
      </div>
     @endforeach
    
  2. 自定义错误页面

     <?php 
     namespace App\Exceptions; 
     use Exception; 
     use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; 
     use Symfony\Component\Debug\ExceptionHandler as SymfonyDisplayer; 
     class Handler extends ExceptionHandler { 
         protected function convertExceptionToResponse(Exception $e) {
             $debug = config('app.debug', false); 
             if($debug) { 
                 return (new SymfonyDisplayer($debug))->createResponse($e);
             }
        
             return response()->view('errors.default', ['exception' => $e], 500);
         }
     }
    

集合篇

  1. 集合筛选

     $customers = Customer::all();
     $us_customers = $customers->filter(function ($customer) {
         return $customer->country == 'United States';
     });
        
     $non_uk_customers = $customers->reject(function ($customer) {
         return $customer->country == 'United Kingdom';
     });
    
  2. find,where

     $collection = App\Person::find([1, 2, 3]);
     $collection = App\Person::all();
     $programmers = $collection->where('type', 'programmer');
     $critic = $collection->where('type', 'critic');
     $engineer = $collection->where('type', 'engineer');
    
  3. closures排序

     $collection = collect([
         ['name' => 'Desk'],
         ['name' => 'Chair'],
         ['name' => 'Bookcase']
     ]);
    
     $sorted = $collection->sortBy(function ($product, $key) {
         return array_search($product['name'], [1 => 'Bookcase', 2 => 'Desk', 3 => 'Chair']);
     });
    
  4. 提取数组的值作为键

     $library = $books->keyBy('title');
        
     [
         'Lean Startup' => ['title' => 'Lean Startup', 'price' => 10],
         'The One Thing' => ['title' => 'The One Thing', 'price' => 15],
         'Laravel: Code Bright' => ['title' => 'Laravel: Code Bright', 'price' => 20],
         'The 4-Hour Work Week' => ['title' => 'The 4-Hour Work Week', 'price' => 5],
     ]
    
  5. collection unions

    没明白。

     // the point is to actually combine results from different models
     $programmers = \App\Person::where('type', 'programmer')->get();
     $critic = \App\Person::where('type', 'critic')->get();
     $engineer = \App\Person::where('type', 'engineer')->get();
    
     $collection = new Collection;
    
     $all = $collection->merge($programmers)->merge($critic)->merge($engineer);
    
  6. collection lookaheads

    没明白。

     $collection = collect([1 => 11, 5 => 13, 12 => 14, 21 => 15])->getCachingIterator();
     foreach ($collection as $key => $value) {
         dump($collection->current() . ':' . $collection->getInnerIterator()->current());
     }
    

中间件/服务篇

  1. 共享cookies

     // app/Http/Middleware/EncryptCookies.php
     protected $except = [
         'shared_cookie'
        
     ];
        
     Cookie::queue('shared_cookie', 'my_shared_value', 10080, null, '.example.com');
    
  2. 有条件的Provider

     // app/Providers/AppServiceProvider.php
     public function register()
     {
         $this->app->bind('Illuminate\Contracts\Auth\Registrar', 'App\Services\Registrar');
        
         if($this->app->environment('production')) {
             $this->app->register('App\Providers\ProductionErrorHandlerServiceProvider');
         } else {
             $this->app->register('App\Providers\VerboseErrorHandlerServiceProvider');
         }
     }
    

路由篇

  1. 嵌套路由

     Route::group(['prefix' => 'account', 'as' => 'account.'], function () {
        
         Route::get('login', ['as' => 'login', 'uses' => 'AccountController@getLogin']);
         Route::get('register', ['as' => 'register', 'uses' => 'AccountController@getRegister']);
        
         Route::group(['middleware' => 'auth'], function () {
             Route::get('edit', ['as' => 'edit', 'uses' => 'AccountController@getEdit']);
         });
        
     });
        
     <a href="\{\{ route('account.login') }}">Login</a>`
     <a href="\{\{ route('account.register') }}">Register</a>`
     <a href="\{\{ route('account.edit') }}">Edit Account</a>`
    
  2. 匹配所有路由

     // app/Http/routes.php
        
     Route::group(['middleware' => 'auth'], function () {
         Route::get('{view}', function ($view) {
             try {
                 return view($view);
             } catch (\Exception $e) {
                 abort(404);
             }
         })->where('view', '.*');
     });
    

其他篇

  1. Model支持多语言

     // database/migrations/create_articles_table.php
     public function up()
     {
       Schema::create('articles', function (Blueprint $table) {
           $table->increments('id');
           $table->boolean('online');
           $table->timestamps();
       });
     }
    
     //database/migrations/create_articles_table.php
     public function up()
     {
       $table->increments('id');
       $table->integer('article_id')->unsigned();
       $table->string('locale')->index();
       $table->string('name');
       $table->text('text');
       $table->unique(['article_id', 'locale']);
       $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade');
    
     }
    
     // app/Article.php
     class Article extends Model
     {
       use \Dimsav\Translatable\Translatable;
       public $translatedAttributes = ['name', 'text'];
     }
    
     // app/ArticleTranslation.php
     class ArticleTranslation extends Model
     {
       public $timestamps = false;
     }
    
     // app/http/routes.php
    
     Route::get('{locale}', function ($locale) {
       app()->setLocale($locale);
       $article = Article::first();
       return view('article')->with(compact('article'));
     });
    
     // resources/views/article.blade.php
    
     <h1>\{\{ $article->name }}</h1>
    
     \{\{ $article->text }}        
    
  2. 定时清理日志

     $schedule->call(function () {
         Storage::delete($logfile);
     })->weekly();
    
  3. pipeling

     $result = (new Illuminate\Pipeline\Pipeline($container)
         ->send($something)
         ->through('ClassOne', 'ClassTwo', 'ClassThree')
         ->then(function ($something) {
             return 'foo';
         });
    

测试篇

  1. 环境变量

     // phpunit.xml
        
     <php>
         <env name="APP_ENV" value="testing"/>
         <env name="CACHE_DRIVER" value="array"/>
         <env name="SESSION_DRIVER" value="array"/>
         <env name="QUEUE_DRIVER" value="sync"/>
         <env name="DB_DATABASE" value=":memory:"/>
         <env name="DB_CONNECTION" value="sqlite"/>
         <env name="TWILIO_FROM_NUMBER" value="+15005550006"/>
     </php>
        
        
     //	.env.test – add to .gitignore
     TWILIO_ACCOUNT_SID = fillmein
     TWILIO_ACCOUNT_TOKEN = fillmein
        
        
     //	access directly from your tests using helper function
     env('TWILIO_ACCOUNT_TOKEN');
        
        
      // tests/TestCase.php 
      <?php 
      class TestCase extends Illuminate\Foundation\Testing\TestCase { 
         
      /** 
      * The base URL to use while testing the application. 
      * 
      * @var string 
      */ 
      protected $baseUrl = 'http://localhost'; 
      /** 
      * Creates the application. 
      * 
      * @return \Illuminate\Foundation\Application 
      */ 
      public function createApplication() { 
          $app = require __DIR__ . '/../bootstrap/app.php';
          if(file_exists(dirname(__DIR__) . '/.env.test')) {
             Dotenv::load(dirname(__DIR__), '.env.test'); 
          } 
             
          $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
                     
          return $app;
      }   }
    
  2. 自动测试

     // gulpfile.js
    
      var elixir = require('laravel-elixir');
    
      mix.phpUnit();
    
      $ gulp tdd
    

参考资料


几个 IntelliJ IDEA 小技巧

Idea 可谓我的主力IDE,吃饭的家伙。感觉 Idea 太大了没办法面面俱到,下面就列一些常用容易遗忘的技巧。

关于 IDEA

IDEA 全称 IntelliJ IDEA,JetBrains 公司旗下产品。IntelliJ在业界被公认为最好的java开发工具之一,尤其在智能代码助手、代码自动提示、重构、J2EE支持、Ant、JUnit、CVS整合、代码审查、 创新的GUI设计等方面的功能可以说是超常的。另外通过插件,Idea 对 PHP 和前端开发也有强力的支持。个人感觉 IntelliJ IDEA 是目前所有 IDE 中最具备沉浸式的 IDE。

JetBrains 公司下的其他产品包括:

  • PhpStorm 主要用于开发 PHP
  • RubyMine 主要用于开发 Ruby
  • PyCharm 主要用于开发 Python
  • AppCode 主要用于开发 Objective-C / Swift
  • CLion 主要用于开发 C / C++
  • WebStorm 主要用于开发 JavaScript、HTML5、CSS3 等前端技术
  • 0xDBE 主要用于开发 SQL
  • Android Studio 主要用于开发 Android(Google 基本 IntelliJ IDEA 社区版进行迭代所以也姑且算上)

常用快捷键

还有不少是与 Vim 重合的快捷键,就不列出来了。作为开发者,Vim 和 Emacs 少是会一个吧。


PHPDoc 入门

概况

这篇文章的目标是了解 PHPDoc 的使用,文末附上了两个开源项目作为参考。关于 PHPDoc 的安装办法可以查看上一篇文章

DocBlocks 以注释的形式出现,但又不仅仅是注释。它是一份行内的文档,可以让你回想起这一块代码是用来干什么的,phpdoc也根据它来生成文档。

DocBlock使用范围包括

  • 函数(function)
  • 常量
  • 接口
  • trait
  • 类内常量
  • 类属性
  • 类方法(method)
  • include require

一个标准的 DocBlock 像这样子:

 <?php
 /**
  * 这是关于这个function的总结。
  *
  * 这是关于function的 *详细描述* 。
  * 可以使用markdown样式
  *
  * @param 这是输入标签。
  *    下面的return是返回值标签。
  *
  * @return void
  */
  function myFunction($myArgument)
  {
  }

运行

$ phpdoc -d path/to/my/project -f path/to/an/additional/file -t path/to/my/output/folder

运行phpdoc时,需要指定扫描的文件夹或者文件,或者事先在配置文件 phpdoc.xml 中声明。 如果没有指定导出文件夹,则默认导出到当前目录下的output。

配置文件 phpdoc.xml 一般放在项目目录下。全局配置文件 phpdoc.dist.xml 在 phpdoc 的安装目录下,也可以修改。

模板

模板文件在 PHPDoc 的安装目录的 data\templates 目录下。

$ phpdoc -d "./src" -t "./docs/api" --template="checkstyle"  // 使用一个模板
$ phpdoc -d "./src" -t "./docs/api" --template="checkstyle,clean" // 使用多个模板
$ phpdoc -d "./src" -t "./docs/api" --template="data/templates/my_template" // 使用自定义模板

我们可以使用 XSL 或者 Twig 模版引擎来自定义模板。phpDoc 提供了很多便利的方法。希望自定义折腾的可以去试试。默认的模板clean我并不喜欢,我选了颜值最高的responsive模板。样式如下:

 phpdoc --template="responsive" -d ./app -t ./docs

与IntelliJ IDEA结合

大部分人写代码还是需要IDE的。可以帮忙做诸如自动补全、预编译报警等等很多事情,减少问题,加快开发进度。我常用的是 JetBrains 厂的 IntelliJ Idea。 PHPDoc 和 IDEA 的结合还是蛮可以的。从 PHPDoc 的官网上就可以看出来, JeyBrains 还是他们的赞助商。

一些简单的使用方法可以在 Idea 官网找到。最常用的就是在方法或者类名之前输入 /** 后按回车键,自动将常用的 Tag 给写好了。

除了写代码时的便利,导出也可以借由 IDEA 完成。

菜单栏 Run -> Edit Configurations,增加php脚本配置。在 File 栏目填写 phpdoc 的路径,例如我的 C:\my_pp\php\php-5.5.30-nts-Win32-VC11-x64\phpdoc,在 Arguments 栏目填写运行变量,例如我的 --template="responsive" -d "C:\Workspace\wechat.kelu.org\app" -t "C:\Workspace\wechat.kelu.org\docs"

Docblocks详解

Docblocks 使用 Tag 的形式来标记。

标记 用途 描述
@abstract   抽象类的变量和方法
@access public, private or protected 文档的访问、使用权限. @access private 表明这个文档是被保护的。
@author 张三 zhangsan@163.com 文档作者
@copyright 名称 时间 文档版权信息
@deprecated version 文档中被废除的方法
@deprec   同 @deprecated
@example /path/to/example 文档的外部保存的示例文件的位置。
@exception   文档中方法抛出的异常,也可参照 @throws.
@global 类型:$globalvarname 文档中的全局变量及有关的方法和函数
@ignore   忽略文档中指定的关键字
@internal   开发团队内部信息
@link URL 类似于license 但还可以通过link找到文档中的更多个详细的信息
@name 变量别名 为某个变量指定别名
@magic   phpdoc.de compatibility
@package 封装包的名称 一组相关类、函数封装的包名称
@param 如 [$username] 用户名 变量含义注释
@return 如 返回bool 函数返回结果描述,一般不用在void(空返回结果的)的函数中
@see 如 Class Login() 文件关联的任何元素(全局变量,包括,页面,类,函数,定义,方法,变量)。
@since version 记录什么时候对文档的哪些部分进行了更改
@static   记录静态类、方法
@staticvar   在类、函数中使用的静态变量
@subpackage   子版本
@throws   某一方法抛出的异常
@todo   表示文件未完成或者要完善的地方
@var type 文档中的变量及其类型
@version   文档、类、函数的版本信息

实例参考

下面参考一下两个开源项目的 DocBlocks。

  1. October

     /**
      * Administrator user model
      *
      * @package october\backend
      * @author Alexey Bobkov, Samuel Georges
      */
     class User extends UserBase
     {
         /**
          * @var string The database table used by the model.
          */
         protected $table = 'backend_users';
    
         /**
          * Validation rules
          */
         public $rules = [
             'email' => 'required|between:6,255|email|unique:backend_users',
             'login' => 'required|between:2,255|unique:backend_users',
             'password' => 'required:create|between:4,255|confirmed',
             'password_confirmation' => 'required_with:password|between:4,255'
         ];
    
         /**
          * Relations
          */
         public $belongsToMany = [
             'groups' => ['Backend\Models\UserGroup', 'table' => 'backend_users_groups']
         ];
    
         public $attachOne = [
             'avatar' => ['System\Models\File']
         ];
    
         /**
          * Purge attributes from data set.
          */
         protected $purgeable = ['password_confirmation', 'send_invite'];
    
         /**
          * @var string Login attribute
          */
         public static $loginAttribute = 'login';
    
         /**
          * @return string Returns the user's full name.
          */
         public function getFullNameAttribute()
         {
             return trim($this->first_name . ' ' . $this->last_name);
         }
    
         /**
          * Gets a code for when the user is persisted to a cookie or session which identifies the user.
          * @return string
          */
         public function getPersistCode()
         {
             // Option A: @todo config
             // return parent::getPersistCode();
    
             // Option B:
             if (!$this->persist_code) {
                 return parent::getPersistCode();
             }
    
             return $this->persist_code;
         }
         ......
     }
    
  2. BootstrapCMS

    这个项目的开发者也是 laravel 开发组成员。

     /*
      * This file is part of Bootstrap CMS.
      *
      * (c) Graham Campbell <graham@alt-three.com>
      *
      * For the full copyright and license information, please view the LICENSE
      * file that was distributed with this source code.
      */
    
     namespace GrahamCampbell\BootstrapCMS\Models\Relations;
    
     /**
      * This is the has many comments trait.
      *
      * @author Graham Campbell <graham@alt-three.com>
      */
     trait HasManyCommentsTrait
     {
         /**
          * Get the comment relation.
          *
          * @return \Illuminate\Database\Eloquent\Relations\HasOneOrMany
          */
         public function comments()
         {
             return $this->hasMany('GrahamCampbell\BootstrapCMS\Models\Comment');
         }
    
         /**
          * Delete all comments.
          *
          * @return void
          */
         public function deleteComments()
         {
             foreach ($this->comments()->get(['id']) as $comment) {
                 $comment->delete();
             }
         }
     }
    

参考资料


PHPDoc 介绍与安装

PHPDoc是一个用PHP写的强大的文档自动生成工具,对于有规范注释的php程序,能够快速生成具有结构清晰、相互参照、索引等功能的API文档。

PHPDoc的原理是: 扫描指定目录下面的php源代码,扫描其中的关键字,截取需要分析的注释,然后分析注释中的专用的tag,生成xml文件,接着根据已经分析完的类和模块的信息,建立相应的索引,生成xml文件对于生成的xml文件,使用定制的模板输出为html文件。

目前,PHPDoc的分析结果可以以HTML形式表现,由于使用了模板机制,可以很方便地定制风格。同时也提供了相应的接口,可以把API文档生成其他的形式,比如PDF,LATEX,WORD等。

以下是我的安装和使用过程。

增加php环境依赖

PHPDoc 依赖于 xsl 和 intl 插件,如果 php.ini 没有打开这两个插件的话要预先打开。当然也可以直接跳过,后边安装不了会弹出错误的,例如:

phpdocumentor/template-zend 1.3.2 requires ext-xsl * -> the requested PHP extension xsl is missing from your system.
zendframework/zend-i18n 2.1.6 requires ext-intl * -> the requested PHP extension intl is missing from your system.

找到你的 php.ini 文件,新增

extension=php_xsl.dll
extension=php_intl.dll

PHPDoc安装方式有三种

  1. PEAR
  2. PHAR
  3. Composer

composer方式尝试之后发现与其它插件依赖冲突,Can’t install with Composer - github issues,于是换用了 PEAR 方式。其他方式也可以参考官网

安装PEAR

下载文件 http://pear.php.net/go-pear.phar 放到 php 目录下。使用管理员权限下打开 cmd/powershell,执行

php go-pear.phar

一路回车即可。这个安装会询问几个问题,主要意思是

  • 以全局模式安装或者本地模式拷贝(应该是绿色安装不写入注册表的意思)
  • 确认安装目录
  • 确认修改 php.ini 文件

安装完成后提示注册环境变量。在安装目录下会生成 pear_env.reg 文件,双击即可。然后检查是否安装成功:

pear -h

最后注册pear的channel(不知道怎么翻译比较好了)

pear channel-discover pear.phpdoc.org

安装 PHPDoc

pear install phpdoc/phpdocumentor

于是就安装完成了。

PHPDoc的简单使用

最简单的命令是:

phpdoc -d [SOURCE_PATH] -t [TARGET_PATH]

-d  这个目录代表着需要生成文档的原始php文件目录(注意是目录) 
-t  这个目录代表着生成的文档存放目录

例如:

phpdoc -f baseTags.php -t docs

搭起服务器就可以访问了。或者直接本地打开index.html文件也可以查看。

要想获得更多参数说明, phpdoc -h即可。因为phpdoc可以使用模板,可以在官网上选择你中意的模板再导出。默认的样式如下图:

tips:phpdoc的中文文档真的很少,要深入使用还是尽量在官网上看。

小问题

  • GraphViz not installed

    在终端运行phpdoc时你可能会遇到如下问题(略过不解决也没问题的样子):

      Unable to find the `dot` command of the GraphViz package. Is GraphViz correctly installed and present in your path?
    

    这是由于系统没有安装 GraphViz 的原因。官网上下载GraphViz:http://www.graphviz.org/Download_windows.php 然后增加环境变量,例如我的是 C:\my_pp\Graphviz2.38\bin

  • Phpdoc No Summary found for this file

    在生成的文档页面中会有错误提示。其中有一个诡异的错误

      Type        Line    Description
      error       0       No summary was found for this file  
    

    具体的原因这个 Stackoverflow 回答的很好 —— 《Phpdoc No Summary found for this file》。以下是我的解决办法,在文件头部加上如下信息:

      /**
       * Class Category | Notification/NtCenter.php
       *
       * @package App\Notification\Models
       * @author kelvinblood <admin@kelu.org>
       * @version     v0.0.1 (2017-3-6)
       * @copyright   Copyright (c) 2017, kelu.org
       */
    

参考资料


laravel phpunit中使用namespace

原先项目并没有做单元测试。今天写了几个hello world,发现使用 namespace 时候报错

PHP Fatal error:  Class 'Tests\TestCase' not found in C:\Workspace\xxx\tests\Unit\ExampleTest.php on line 10

解决办法:

修改composer.json,增加如下设置:
   
"autoload-dev": {
   "psr-4": {
       "Tests\\": "tests"
   }

修改完成后 composer install 重新加载项目即可。

关于新增的autoload-dev的作用,以及composer.json文件的解释,下篇文章写一个。

参考资料


composer.json 配置文件说明

Java有Maven, Node.js有npm, ROR有gem,PHP有composer. 他们都是各个语言的包管理器。

下面将以一个composer.json为例,简单介绍Composer的使用方法。完整的文档可以查看最后的参考资料。

{
// ================================ 配置文件 ================================
    "name":             "kelu/app",
    "description":      "血衫非弧的app",
    "keywords":         ["kelvinblood", "kelu", "app"],
    "homepage":         "http://app.kelu.org ",
    "time":             "2016-12-30",
    "license":          "MIT",
    "authors": [{
        "name":         "Kelvin Blood",
        "email":        "xxx@xxx.org",
        "homepage":     "http://www.kelu.org",
        "role":         "CEO"
    }],
    "type": "project", // library project metapackage composer-plugin
    "repositories": [
        {
            "type": "vcs",
            "url": "https://git.oschina.net/apkj/phpwebframework.git"
        }
    ],
// ================================ 依赖管理 ================================
// === 默认情况下,composer只会获取稳定版本,修改后运行命令 composer install ===
    "require": {
        "php": ">=5.5.9",
        "laravel/framework": "5.1.*",
        "ignited/laravel-omnipay": "2.*",
        "lokielse/omnipay-alipay": "dev-master"
    },
// === 有些包依赖只会在开发过程中使用,正式发布的程序不需要这些包,这个时候,就需要用到另外一个键,即require-dev。例如,我们用phpunit单元测试,那么就可以通过require-dev引入这个开发环境下的依赖包
    "require-dev": {
        "fzaninotto/faker": "~1.4",
        "mockery/mockery": "0.9.*",
        "phpunit/phpunit": "~4.0",
        "phpspec/phpspec": "~2.1"
    },
    
// ================================ 自动加载 ================================
// === 加载文件最简单的方式就是require或者include, autoload,顾名思义,就是自动加载. 
// === 修改后,运行命令: composer dump-autoload, 让composer重建自动加载的信息
// === composer 提供了4种自动加载类型 classmap psr-0 psr-4 files 
// === files,对应的值是一个数组,数组元素是文件的路径,路径是相对于应用的根目录。
// === classmap,会在背后就会读取这个文件夹中所有的文件 然后再 vendor/composer/autoload_classmap.php 中怒将所有的 class 的 namespace + classname 生成成一个 key => value 的 php 数组.缺点是一旦增加了新文件,需要执行dump-autoload命令重新生成映射文件。
// === psr-0 现在这个标准已经过时
// === psr-4 支持将命名空间映射到路径。命名空间结尾的\\不可省略。当执行install或update时,加载信息会写入vendor/composer/autoload_psr4.php文件。如果希望解析指定路径下的所有命名空间,则将命名空间置为空串即可。
    "autoload": {
     "files":["lib/OrderManager.php"],
        "classmap": [
            "database"
        ],
         // FIG组织制定的一组PHP相关规范,简称PSR,其中PSR-0自动加载 PSR-1基本代码规范 PSR-2代码样式 PSR-3日志接口 PSR-4 自动加载
        "psr-4": {
            "App\\": "app/"         // 自动加载命名空间App,文件夹app里的文件
        }
    },
// 和require-dev类似,只在开发过程中自动加载
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests" // 自动加载Tests的命名空间
        }
    },
    
// ================================ 脚本 ================================
// === 在安装过程中的各个阶段挂接脚本。
    "scripts": {
        "post-install-cmd": [
            "php artisan clear-compiled",
            "php artisan optimize"
        ],
        "pre-update-cmd": [
            "php artisan clear-compiled"
        ],
        "post-update-cmd": [
            "php artisan clear-compiled",
            "php artisan optimize"
        ],
        "post-root-package-install": [
            "php -r \"copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "php artisan key:generate"
        ]
    },
    
// ================================ 设置 ================================
    "config": {
        "preferred-install": "dist", //Composer 的默认安装方法。 source、dist 或 auto
        "secure-http": false
    }
}

参考资料

参考资料


关于软件测试

最近在看关于软件测试方面的资料,整理了一些资料。文末列出了一些关于软件测试的观点,顺便推荐两个测试做的比较好的php项目。

何为软件测试

使用人工或自动的手段来运行或者测量软件系统的过程,以检验软件系统是否满足规定的要求,并找出与预期结果之间的差异。

软件测试应当遵循的原则

  • 测试显示缺陷的存在,但不能证明没有缺陷
  • 穷尽测试是不可能的,应设定及时终止的条件
  • 测试应尽早进行
  • 缺陷具备群集特性
  • 测试的杀虫剂悖论,不定期增删修改测试用例,从而发现软件缺陷
  • 测试的二八原则,80%的时间和资源用在20%的重点模块上
  • 测试活动依赖于测试背景,针对不同的测试背景,测试活动场景要求也是不一样的,比如对电信大并发量性能、金融的安全要求等.

软件测试的分类

按阶段划分

  1. 单元测试

    单元测试是对软件中的基本组成单位进行的测试。目的是检验软件基本组成单位的正确性。单元测试可以倒推要求开发人员正视需求,对自己的程序更有信心。

  2. 集成测试

    在单元测试的基础上,将所有软件单元按照概要设计规格说明的要求组装成模块、子系统或者系统的过程中各部分工作是否达到或实现相应技术指标以及要求的活动。偏于从技术角度进行测试实施方案

  3. 系统测试

    将各子系统结合起来,包括功能测试、非功能测试。非功能测试就包括很多了,性能测试、稳定性测试、可用性测试、安全测试等。注重整个系统完整的功能和性能,偏于从业务角度测试系统。

    • 功能测试:

       功能测试是对产品的各功能进行验证,以检查是否满足需求的要求。 其中又分为很多种:逻辑功能测试、界面测试、易用性测试、兼容性测试 一般功能测试使用的工具有:

      商用

      • QTP
      • WinRunner
      • silktest
      • rational robot 开源
      • selenium
      • watir
      • sikuli
    • 性能测试:

       性能测试是通过自动化测试工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测试。软件的性能包括很多方面,主要有时间性能和空间性能两种。 时间性能:主要是指软件的一个具体的响应时间。比如一个登录所需要的时间,一个交易所需要的时间等。当然,抛开具体的测试环境,来分析一次事务的响应时间是没有任何意义的。需要搭建一个具体且独立的测试环境。 空间性能:主要指软件运行时所消耗的系统资源,比如硬件资源,CPU、内存,网络带宽消耗等。 一般功能测试使用的工具有:

      • loadrunner
      • silkperformer
      • jmeter
      • webload
      • apache bench
      • loadui
    • 安全测试:

      安全测试检查系统对非法入侵的防范能力。这一类的工具有:

      • appscan web应用漏洞扫描
      • webinspect web应用漏洞扫描
      • nessus 服务器主机类漏洞扫描
      • nmap 端口嗅探
      • metasploit 渗透测试
      • webscarab 代理劫持分析
      • fortify 白盒测试工具,源代码静态分析
      • w3af Web应用程序攻击和检查框架
    • 兼容测试:

        兼容性测试主要是测试系统在不同的软硬件环境下是否能够正常的运行。

  4. 交付测试/验收测试

    • 功能确认测试
    • 安全可靠性测试
    • 易用性测试
    • 可扩充性测试
    • 兼容性测试
    • 资源占用率测试
    • 用户文档资料验收

按照对象可见度

黑盒测试、白盒测试、灰盒测试

按照状态

静态测试、动态测试

按照测试执行方法

  1. 手工测试

    手工测试就是由人去一个一个的去执行测试用例,通过键盘鼠标等输入一些参数,查看返回结果是否符合预期结果。

  2. 自动化测试

    自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程。又可分为功能自动化测试与性能自动化测试。

冒烟测试、回归测试、随机测试

  这三种测试在软件功能测试过程中,既不算具体明确的测试阶段也不算是具体的测试方法。

  1. 冒烟测试

    是指在对一个新版本进行系统大规模的测试之前,先验证一下软件的基本功能是否实现,是否具备可测性。 引入到软件测试中,就是指测试小组在正规测试一个新版本之前,先投入较少的人力和时间验证一个软件 的主要功能,如果主要功能都没有实现,则打回开发组重新开发。这样做的好处是可以节省大量的时间成本和人力成本。

  2. 回归测试

    是指修改了旧代码后,重新时行测试以确认修改后没有引入新的错误或导致其他代码产生错误。 回归测试一般是在进行软件的第二轮测试开始的,验证第一轮中发现的问题是否得到修复。当然,回归也是一个循环的过程,如果回归的问题通不过,则需要开发人员修改后再次进行回归,直到通过为止。

  3. 随机测试

    是指测试中的所有输入数据都是随机生成的,其目的是模拟用户的真实操作,并发现一些边缘性的错误。 随机测试可以发现一些隐蔽的错误,但是也有很多缺点,比如测试不系统,无法统计代码覆盖率和需求覆盖率,发现的问题难以重现。一般是放在测试的最后执行。其实随机测试更专业的升级版叫 探索性测试

  4. 探索性测试

    探索性测试可以说是一种测试思维技术。它没有很多实际的测试方法、技术和工具。探索性强调测试人员的主观能动性,抛弃繁杂的测试计划和测试用例设计过程,强调在碰到问题时及时改变测试策略。

一些观点

企业级Web项目中应该如何做单元测试、集成测试和功能测试?

Q: 使用Java做企业市场产品开发,应该如何做单元测试, 集成测试和功能测试?是只测试后端服务吗?需不需要做界面的测试?之前和业界某著名咨询公司首席咨询师交流,他说应该多做基于场景的测试,并且尽量测试真实的代码,少打桩,这种说法有什么问题?

A: 并不是单元测试本身不好,而是由于单元测试本身花费的时间量太大,导致真正能够做好单元测试的越来越少。当前很多企业应用开发,根本没有逻辑层,或者说没有明确的领域服务,写出的单元测试用例直接在跑数据库的CRUD,这个基本没有太大的意义。

个人认为单元测试重点一定是要有明确的领域层或服务层,同时单元测试最好也是和CI持续集成配合,即单元测试无法做到100%完全测试用例覆盖,重点还是主体功能覆盖,更好的用于CI中的自动化冒烟测试。

集成测试,能够做好的同样很少,很多集成测试就是在做系统测试,原因在于很多就没有子系统的概念,没有组件化和模块化的概念,接口本身也没有提前定义和设计,谈不上模块的集成和组装。集成测试没做好不是方法的问题,首先还是模块化和接口设计没推进的问题。当有了真正的集成测试,你就会明白stub的使用往往是必须的了,特别是在由顶向下进行的集成的时候。

以上两点都难推进的情况下,那就好好把系统测试做好,你说的是对的,基于业务场景的系统功能测试是最重要的,至少能够交付到客户一个缺陷率低,满足业务需求的产品。

单元测试的好处

A:单元测试最直接的好处有两点:

  • 让你写出更好的代码:职业高内聚、低耦合而且接口设计合理的代码才易于测试
  • 让你在修改代码时更有信心

如何保持unit test代码的稳定

A: 代码是为了什么,当然是为了重复运行。如何保持unit test代码的稳定?主要靠好的API设计。API切实正确切割了需求,那么在重构的时候API就基本不用变化,unit test也不用重写。以后你重构的时候,只要你的unit test覆盖的够好,基本跑一遍就知道有没有改成傻逼。可以节省大量的时间。

所以那些专门写不需要维护的软件的人,讨厌测试,也是情有可原的。

两个PHP项目

BootstrapCMS

项目作者是PHP著名的 Laravel 框架的开发组成员。

octobercms

这个项目的测试文件比较完备,甚至改包括了使用selenium进行UI测试。

参考资料