惠吃猫-重构-API接口
基于二次开发,无法继续开发后的一次重构,使用lumen重构API接口
引入多个 route 文件
在 routes 文件夹下创建一个路由文件,比如 api.php,然后在 bootstrap/app.php 中找到并添加:
1 2 3 4
| $app->group(['namespace' => 'App\Http\Controllers'], function ($app) { require __DIR__.'/../routes/web.php'; require __DIR__.'/../routes/api.php'; });
|
为路由定义可选参数
Lumen 不支持。
在中间件中获取路由参数
如下两种 Lumen 不支持但 Laravel 支持:
1 2 3 4 5
| public function handle($request, Closure $next) { $request->route()->parameters(); $request->route('parameter'); }
|
Lumen
只能使用:$request->route()[2]
这个数组,里面就是路由参数。
路由定义和命名空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php $app->group([ 'namespace' => 'ThirdParty', ], function () use ($app) { $app->group([ 'prefix' => 'qiniu', 'middleware' => [ 'qiniu_auth', ] ], function () use ($app) { $app->group([ 'middleware' => [ 'jwt_auth', ], ], function () use ($app) { $app->get('up_ticket', 'Qiniu@getUploadTicket'); }); $app->post('upload_cbk', [ 'as' => 'qiniu_upload_callback', 'uses' => 'Qiniu@uploadCallback', ]); }); });
|
在这种路由定义方式中,因为路由文件中默认的命名空间是 App\Http\Controllers
,因此如果需要给路由定义 namespace
,比如这里的 ThirdParty
,则无需再写成:App\Http\Controllers\ThirdParty
,因为它会自动在 App\Http\Controllers
命名空间下找 ThirdParty
这个路径,不会报错。
同理,上面的 Qiniu@getUploadTicket
也无需写成 App\Http\Controllers\ThirdParty\Qiniu@getUploadTicket
,因为在这个群路由定义的开始已经指定为 ThirdParty
了,下级在找路由器的时候便会自动去 App\Http\Controllers\ThirdParty
这个命名空间找,这样一层一层,以此类推。
重定向至命名路由并传递参数
1
| return redirect()->route('admin', ['key' => 'value']);
|
启用自定义 JWT 认证
1 2 3
| $app->routeMiddleware([ 'auth_jwt' => App\Http\Middleware\Auth\JWT::class, ]);
|
- 然后修改
app/Http/Middleware/Auth/JWT.php
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?php
namespace App\Http\Middleware\Auth; use Closure; use Firebase\JWT\JWT as FirebaseJWT; class JWT { protected $auth = false; public function __construct() { try { if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $this->auth = FirebaseJWT::decode( $_SERVER['HTTP_AUTHORIZATION'], env('APP_KEY'), ['HS256'] ); } } catch (\Exception $e) { } finally { } } public function handle($request, Closure $next) { if (false === $this->auth) { return response()->json([ 'error' => 'Unauthorized' ], 401); } $request->attributes->add([ 'auth_jwt' => $this->auth, ]); return $next($request); } }
|
Pass value from middleware to controller
将中间件的值传递给控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public function handle($request, Closure $next) { $request->attributes->add([ 'key' => 'value' ]); return $next($request); }
public function getVauleFromMiddleware(Request $req) { echo $req->get('key'); }
|
通过这种方式,可以对不同的路由使用不同的认证(非常语义化),然后把各种认证的结果(jwt,七牛)传递到控制器,可以使控制器不用再操心认证逻辑,减少控制器代码量。
自定义配置文件
1
| $app->configure('custom');
|
- 创建一个
config/custom.php
文件
1 2 3 4
| <?php return [ 'key' => 'value' ];
|
1 2 3 4 5 6
| dd(config('custom')); /* output: array:1 [▼ "key" => "value" ] */
|
CORS
如果是客户端是浏览器,则在调用 API
的时候,基本上无法避免跨域问题。
不过处理也很简单,需要浏览器和服务器同时配合:浏览器在发起 OPTIONS preflight 请求的时候,服务器返回认可的一些信息就行了。
具体如下:
app/Http/Middleware/CORS.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php
namespace App\Http\Middleware; use Closure;
class CORS { public function __construct() { } public function handle($request, Closure $next) { $headers = [ 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Methods' => '*', 'Access-Control-Allow-Headers' => 'Access-Control-Allow-Origin, AUTHORIZATION', 'Access-Control-Max-Age' => 86400, ]; if ('OPTIONS' == $request->getMethod()) { return response(null, 200, $headers); } return $next($request)->withHeaders($headers); } }
|
1 2 3 4
| $app->middleware([ 'cors' => App\Http\Middleware\CORS::class, ]);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $.ajax({ url: 'http://api.example.com/path/to/resource', type: 'GET', dataType: 'json', async: false, cors: true , headers: { 'Access-Control-Allow-Origin': '*', 'AUTHORIZATION': 'JWT_STRING' }, success: function (res) { }, error: function (xhr, status) { } });
|
migration
中给表加注释
1 2 3 4 5 6 7
| public function up() { Schema::create('table_name', function (Blueprint $table) { }); DB::statement('ALTER TABLE `table_name` comment "table_comment"'); }
|
migration
设置 current_timestamp
1 2 3
| $table->dateTime('create_at')->default( DB::raw('CURRENT_TIMESTAMP') );
|
连接多个数据库
1 2
| # bootstrap/app.php $app->configure('database');
|
- 创建并修改
config/database.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php return [ 'default' => 'mysql_db1_rw', 'connections' => [ 'mysql_db1_rw' => [ 'driver' => 'mysql', 'host' => env('DB_HOST'), 'database' => 'db1_rw', 'username' => env('DB_USERNAME'), 'password' => env('DB_PASSWORD'), 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', ], 'mysql_db1_ro' => [ 'driver' => 'mysql', 'host' => env('DB_HOST'), 'database' => 'db1_ro', 'username' => env('DB_USERNAME'), 'password' => env('DB_PASSWORD'), 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', ], ] ];
|
注意:
一旦创建了 config/database.php
文件,系统默认的 vendor/laravel/lumen-framework/config/database.php
将失效。
因此,最好的做法是拷贝系统自带的配置文件到项目根目录,然后进行一些自定义的修改。
1 2
| rm -rf config/*.php cp vendor/laravel/lumen-framework/config/database.php config/
|
根据上面的配置默认会连接 mysql_db1_rw
这个数据库。那如果需要连接第二个数据库,使用示例如下:
1 2 3 4 5 6
| app('db')->connection()->select('show tables'); DB::connection()->select('show tables');
app('db')->connection('mysql_db1_ro')->select('show tables'); DB::connection('mysql_db1_ro')->select('show tables');
|
自定义日志文件名
Lumen
中默认的文件名是被硬编码在 Application
类中了的,要实现这样的效果,必须重写 Laravel\Lumen\Application@getMonologHandler()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php
namespace App;
use Laravel\Lumen\Application as LumenBase; use Monolog\Formatter\LineFormatter; use Monolog\Handler\StreamHandler; use Monolog\Logger;
class Application extends LumenBase { protected function getMonologHandler() { return ( new StreamHandler(storage_path(env( 'APP_LOG_PATH', 'logs/'.date('Y-m-d').'.log' )), Logger::DEBUG )) ->setFormatter(new LineFormatter(null, null, true, true)); } }
|
- 修改 bootstrap/app.php
1 2 3
| $app = new App\Application( realpath(__DIR__.'/../') );
|
自定义 artisan 命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
namespace App\Console\Commands; use Illuminate\Console\Command; class Hello extends Command { protected $name = 'patch:hello'; protected $description = 'Balabalabala'; public function fire() { $this->info('hello atisan cmd'); } }
|
- 创建一个 command service provider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php
namespace App\Providers; use Illuminate\Support\ServiceProvider; class CommandService extends ServiceProvider { public function register() { $this->app->singleton('command.patch.hello', function () { return new \App\Console\Commands\Hello; }); $this->commands( 'command.patch.hello' ); } }
|
- 注册该
command service provider
1 2
| // bootstrap/app.php $app->register(App\Providers\CommandService::class);
|
1 2
| php artisan patch:hello
|
模型查询结果中隐藏部分字段
1 2 3 4 5 6
| protected $hidden = [ 'passwd', ]; protected $visible = [ 'uid' ];
|
其中 $visible 的优先级比 $hidden 高。
FAQ
- docker 搭建的运行环境,macOS 下执行 php artisan migrate 提示:_php_networkgetaddresses: getaddrinfo failed: nodename nor servname provided, or not known?
这是因为是在本地(macOS)执行的这条命令,而本地对 .env 中配置的 DB_HOST=db 是解析不了的。因此会出现这个错误。
解决这种问题也很简单,有两种办法:
安装 memcached/memcache 扩展。
- php artisan migrate PDOException: user not exist?
- 检查是主机地址是否填错。
- 检查配置中的数据库服务器是否存在该用户。
可能踩的坑
检查对象是否是有效的 Model 对象。举例说明,以下代码第一步将不会生效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| $user = User::select('money')->whereUid(1)->first(); \DB::beginTransaction(); // 第一步:减少用户余额 $user->money -= 100; $decreased = $user->save(); // 第二步:记录日志 $loggedId = Log::insertGetId([ 'uid' => 1, 'amount' => -10, 'intro' => 'test', ]); // 两步结果共同决定是否执行事务 if ($decreased && $loggedId) { \DB::commit(); } else { \DB::rollback(); }
|
这是因为,对模型对象调用了 select() 方法后,就不是一个完整的模型对象了(从结果来看)。
解决办法是,不使用 select() 或者使用 find()。
1 2 3
| $user = User::whereUid(1)->first(); // Or $user = User::find(1);
|
参考