Road to growth of rookie

Meaningful life is called life

0%

Laravel/Lumen 在项目中的一些实践

Laravel/Lumen 应该是目前最受欢迎的框架了, 所以它们的社区也是最活跃的, 扩展包扽支持也是最多的; 从 18 年工作开始我们一直都在使用 Laravel/Lumen, 也在项目中对它做出来一些修改, 用于更好、更快的开发项目

关于 DB 类的一些实践

通过 DB::listen 监听所有执行的 SQL 语句, 这个操作也可以通过查看 mysql 的查询日志实现, 但是调试的过程中还是没有直接写一个函数来的方便

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
if (!function_exists('print_sql')) {
/**
* To monitor and print execute SQL statements
*
* @param bool $die
*
* @return void
*/
function print_sql($die = false)
{
\Illuminate\Support\Facades\DB::listen(function ($sql) use ($die) {
$singleSql = $sql->sql;
$function = $die ? 'dd' : 'd';
if ($sql->bindings) {
foreach ($sql->bindings as $replace) {
$value = is_numeric($replace) ? $replace : "'" . $replace . "'";
$singleSql = preg_replace('/\?/', $value, $singleSql, 1);
}
$function($singleSql);
} else {
$function($singleSql);
}
});
}
}
Laravel/lumen 有两种使用事务的方式
  • 一种是通过 DB::rollBack() 开始事务; DB::rollback()DB::commit() 回滚或提交事务
  • 一种是通过 DB::transaction(\Closure $callback) 的方式

推荐的用法是 DB::transaction 的方式, 它的实现:

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
36
37
38
39
/**
* Execute a Closure within a transaction.
* \Illuminate\Database\Concerns\ManagesTransactions
*
* @param \Closure $callback
* @param int $attempts
* @return mixed
*
* @throws \Exception|\Throwable
*/
public function transaction(Closure $callback, $attempts = 1)
{
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
$this->beginTransaction();

// We'll simply execute the given callback within a try / catch block and if we
// catch any exception we can rollback this transaction so that none of this
// gets actually persisted to a database or stored in a permanent fashion.
try {
return tap($callback($this), function () {
$this->commit();
});
}

// If we catch an exception we'll rollback this transaction and try again if we
// are not out of attempts. If we are out of attempts we will just throw the
// exception back out and let the developer handle an uncaught exceptions.
catch (Exception $e) {
$this->handleTransactionException(
$e, $currentAttempt, $attempts
);
} catch (Throwable $e) {
$this->rollBack();

throw $e;
}
}
}

从实现中可以看出, 它也是使用的 beginTransactioncommitrollBack 去控制事务, 但是只要出现 Exception 就会自动会滚, 不用自己去关心事务回滚和提交

引入 Context 的概念

在这整个框架的整个生命周期里, 会有一些数据需要全局共享, 但是 Laravel/Lumen 并没有给出统一的接口; 在觉早期的项目中我们都是存放在 session 中, 后来我们弃用了 session. 所以我们引入了一个 Context 的概念, 将所有的共享数据都放在 Context

Context 的具体实现
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<?php

namespace App\Foundation\Handlers;

use Closure;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Exception;
use Symfony\Component\HttpFoundation\Response;

/**
* Class ContextHandler
* @package App\Foundation\Handlers
*/
class ContextHandler
{
/**
* Example of the current class
* @var $this ;
*/
protected static $instance;
/**
* Data store
* @var array
*/
protected static $data = [];

/**
* The protected constructor prohibits the creation of an
* instance of the current class
*
* ContextHandler constructor.
*/
protected function __construct()
{
}

/**
* The protected clone magic method forbids the current class
* from being cloned
*/
protected function __clone()
{
}

/**
* Returns an instance of the current class
*
* @return $this;
*/
public static function getInstance(): ContextHandler
{
if (!static::$instance) {
static::$instance = new self();
}

return static::$instance;
}

/**
* Set a data to current class
*
* @param string $key
* @param mixed $value
*
* @return mixed
*/
public function set(string $key, $value)
{
if ($value instanceof Closure) {
$value = $value();
}

Arr::set(static::$data, Str::snake($key), $value);

return $value;
}

/**
* Get a data by current class
*
* @param string|array $key
* @param null|mixed $default
*
* @return mixed
*/
public function get($key, $default = null)
{
return Arr::get(static::$data, $key, $default);
}

/**
* Gets part of the specified data from the data set
*
* @param array $keys
*
* @return array
*/
public function only(array $keys): array
{
return Arr::only(static::$data, $keys);
}

/**
* Determine if there is any data in the data set
*
* @param string $key
*
* @return bool
*/
public function has(string $key): bool
{
return array_key_exists($key, static::$data);
}

/**
* @param string $key
* @param mixed $value
*/
public function __set(string $key, $value)
{
$this->set($key, $value);
}

/**
* If $data contains the data that needs to be retrieved,
* return it directly If there is no corresponding
* access method
*
* @param string $key
*
* @return mixed|null
*/
public function __get(string $key)
{
if ($this->has($key)) {
return $this->get($key);
}

$method = 'get'.ucfirst(Str::camel($key)).'Attribute';

if (method_exists($this, $method)) {
$value = $this->$method();

return $this->set($key, $value);
}

return null;
}

/**
* Gets the subscript of the data set based on the method name
* Determine whether there is data in the data set
* Calls private or protected methods when data does not exist
* And stored in the data set for reuse
*
* @param string $method
* @param array $parameters
*
* @return mixed
* @throws Exception
*/
public function __call(string $method, array $parameters)
{
$key = Str::snake($method);

if ($this->has($key)) {
return $this->get($key);
}

if (!method_exists($this, $method)) {
$message = sprintf('Method %s::%s does not exist.', static::class, $method);
throw new Exception($message, Response::HTTP_INTERNAL_SERVER_ERROR);
}

$value = $this->$method(...$parameters);

return $this->set($key, $value);
}

/**
* Gets the routing alias for this request
*
* @return mixed|string
*/
protected function getRouteNameAttribute()
{
$route_params = Request::route() ?: [];
$route_name = '';

foreach ($route_params as $route_param) {
if (is_array($route_param) && array_key_exists('as', $route_param)) {
$route_name = $route_param['as'];
}
}

return $route_name;
}
}
修改 AppServiceProvider 注册单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Providers;

use App\Foundation\Handlers\ContextHandler;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('context', ContextHandler::getInstance());
}
}
添加一个 Facade
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
<?php

namespace App\Foundation\Facades;

use Illuminate\Support\Facades\Facade;

/**
* Class Repositories
* @package App\Foundation\Facades;
*
* @method static mixed set(string $key, $value)
* @method static mixed get($key, $default = null)
* @method static array only(array $keys)
* @method static bool has(string $key)
*/
class Context extends Facade
{
/**
* @return string
*/
protected static function getFacadeAccessor(): string
{
return "context";
}

}

具体的测试逻辑, 我就不写了, 这个概念在我们项目中已经使用了很久, 是一个比较成熟的想法; 类似的实现还有很多, 有人也提出使用 Cache::driver('array') 的方式去共享数据.

引入 Service 模式

在早期的项目里, 我们把所有的业务逻辑都写到 Controller 中, 造成每个 Controller 非常臃肿, 且无法封装重复的代码, 对维护和开发都很不友好; 最后我们选择引入 Service 模式, 将业务逻辑都封装到 Service 中方便维护和开发; 到现在为止, 我们的 Controller 中只有对 Service 的调用和响应返回 (除了是 5 行以内能够解决的逻辑可以直接写到 Controller)

一开始引入 Service 这个概念的时候, 我们通过依赖注入的方式将每个 Service 都注入到 Controller, 但是 Service 也有互相引用的问题, 这样的情况就没有办法依赖注入; 所以我们把所有的 Service 实例都放到了 Context 中, 哪里需要哪里调用

Service 伪单例的具体实现
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php

namespace App\Foundation\Handlers;

use App\Foundation\Facades\Context;
use Illuminate\Support\Str;
use \Exception;

/**
* Class ClassesStorageHandler
* @package App\Foundation\Handlers;
*/
class ClassesStorageHandler
{
/**
* A namespace for the singleton class group is required
*
* @var string
*/
protected $namespace;

/**
* A suffix for the singleton class group is required
*
* @var string
*/
protected $suffix;

/**
* ClassesStorageHandler constructor
*
* @param string $namespace
* @param string $suffix
*/
public function __construct(string $namespace, string $suffix)
{
$this->namespace = $namespace;
$this->suffix = $suffix;
}


/**
* Get the real from the context by calling getXxx
*
* @param string $methodName
*
* @return mixed
* @throws Exception
*/
public function __call(string $methodName)
{
$saveKey = Str::snake(get_class($this));
$objects = Context::get($saveKey, []);
$name = Str::snake(preg_replace('/get/', '', $methodName, 1));

if (array_key_exists($name, $objects)) {
return $objects[$name];
}

$objectName = ucfirst(Str::camel($name));
$this->namespace && $objectName = $this->namespace.$objectName;
$this->suffix && $objectName = $objectName.$this->suffix;

if (!class_exists($objectName)) {
throw new Exception('Class '.$objectName.' Not found');
}

$object = app($objectName);
$objects[$name] = $object;
Context::set($saveKey, $object);

return $object;
}
}
修改 AppServiceProvider 注册单例
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\Providers;

use App\Foundation\Handlers\ClassesStorageHandler;
use App\Foundation\Handlers\ContextHandler;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('context', ContextHandler::getInstance());

/** @var string $servicesNamespace 所有 service命名空间 */
$servicesNamespace = config('app.services_namespace');
$this->app->singleton('services', new ClassesStorageHandler(
$servicesNamespace, 'Service'));
}
}
添加一个 Facade
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\Foundation\Facades;

use App\Repositories\UsersRepository;
use Illuminate\Support\Facades\Facade;

/**
* Class Repositories
* @package App\Foundation\Facades;
*
* @method static UserService getUsers()
*/
class Services extends Facade
{
/**
* @return string
*/
protected static function getFacadeAccessor(): string
{
return "services";
}

}

下面就可以直接全局使用 Services::getUsers() 的方式直接获取到 UserService 的实例

Lumen 中使用 Laravel 的一些组件

LumenLaravel 的精简版, 所有有些 Laravel 功能是没有的, 很奇怪的是, 虽然有些功能没法使用, 但它其实是集成了的, 只是需要额外的去配置才可以 (这一点就真的让我迷的不行, 可能是因为 Laravelcomposer 包并没有分那么细)

Lumen 中使用 Form Request
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<?php

namespace App\Http\Requests;

use Illuminate\Http\Request as IlluminateRequest;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Arr;
use Laravel\Lumen\Http\Redirector;
use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\UnauthorizedException;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Validation\ValidatesWhenResolvedTrait;
use Illuminate\Contracts\Validation\ValidatesWhenResolved;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;

class Request extends IlluminateRequest implements ValidatesWhenResolved
{
use ValidatesWhenResolvedTrait;
/**
* The container instance.
*
* @var \Illuminate\Container\Container
*/
protected $container;
/**
* The redirector instance.
*
* @var \Laravel\Lumen\Http\Redirector
*/
protected $redirector;
/**
* The route to redirect to if validation fails.
*
* @var string
*/
protected $redirectRoute;
/**
* The controller action to redirect to if validation fails.
*
* @var string
*/
protected $redirectAction;
/**
* The key to be used for the view error bag.
*
* @var string
*/
protected $errorBag = 'default';
/**
* The input keys that should not be flashed on redirect.
*
* @var array
*/
protected $dontFlash = ['password', 'password_confirmation'];

/**
* Get the validator instance for the request.
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function getValidatorInstance()
{
$factory = $this->container->make(ValidationFactory::class);
if (method_exists($this, 'validator')) {
return $this->container->call([$this, 'validator'], compact('factory'));
}

return $factory->make($this->validationData(), $this->container->call([$this, 'rules']), $this->messages(),
$this->attributes());
}

/**
* Get data to be validated from the request.
*
* @return array
*/
protected function validationData()
{
return $this->all();
}

/**
* Handle a failed validation attempt.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
*
* @return void
*
* @throws \Illuminate\Http\Exceptions\HttpResponseException
*/
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException($this->response($this->formatErrors($validator)));
}

/**
* Determine if the request passes the authorization check.
*
* @return bool
*/
protected function passesAuthorization()
{
if (method_exists($this, 'authorize')) {
return $this->container->call([$this, 'authorize']);
}

return false;
}

/**
* Handle a failed authorization attempt.
*
* @return void
*
* @throws \Illuminate\Http\Exceptions\HttpResponseException
*/
protected function failedAuthorization()
{
// throw new HttpResponseException($this->forbiddenResponse());
throw new UnauthorizedException($this->forbiddenResponse());
}

/**
* Get the proper failed validation response for the request.
*
* @param array $errors
*
* @return \Illuminate\Http\JsonResponse
*/
public function response(array $errors)
{
return new JsonResponse($errors, 422);
}

/**
* Get the response for a forbidden operation.
*
* @return \Illuminate\Http\Response
*/
public function forbiddenResponse()
{
return new Response('Forbidden', 403);
}

/**
* Format the errors from the given Validator instance.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
*
* @return array
*/
protected function formatErrors(Validator $validator)
{
return $validator->getMessageBag()->toArray();
}

/**
* Set the Redirector instance.
*
* @param \Laravel\Lumen\Http\Redirector $redirector
*
* @return $this
*/
public function setRedirector(Redirector $redirector)
{
$this->redirector = $redirector;

return $this;
}

/**
* Set the container implementation.
*
* @param \Illuminate\Container\Container $container
*
* @return $this
*/
public function setContainer(Container $container)
{
$this->container = $container;

return $this;
}

/**
* Get custom messages for validator errors.
*
* @return array
*/
public function messages()
{
return [];
}

/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes()
{
return [];
}

/**
* Get the route handling the request.
*
* @param string|null $param
* @param mixed $default
*
* @return mixed
*/
public function route($param = null, $default = null)
{
$route = call_user_func($this->getRouteResolver());

if (is_null($route) || is_null($param)) {
return $route;
}

$parameters = end($route);

return Arr::get($parameters, $param, $default);
}
}

添加 RequestServiceProvider

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

namespace App\Providers;

use Laravel\Lumen\Http\Redirector;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\HttpFoundation\Request;
use Illuminate\Contracts\Validation\ValidatesWhenResolved;

class RequestServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
$this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
$resolved->validateResolved();
});
$this->app->resolving(FormRequest::class, function ($request, $app) {
$this->initializeRequest($request, $app['request']);
$request->setContainer($app)->setRedirector($app->make(Redirector::class));
});
}

/**
* Initialize the form request with data from the given request.
*
* @param FormRequest $form
* @param \Symfony\Component\HttpFoundation\Request $current
*
* @return void
*/
protected function initializeRequest(FormRequest $form, Request $current)
{
$files = $current->files->all();
$files = is_array($files) ? array_filter($files) : $files;
$form->initialize($current->query->all(), $current->request->all(), $current->attributes->all(),
$current->cookies->all(), $files, $current->server->all(), $current->getContent());
$form->setJson($current->json());
if ($session = $current->getSession()) {
$form->setLaravelSession($session);
}
$form->setUserResolver($current->getUserResolver());
$form->setRouteResolver($current->getRouteResolver());
}
}