Client.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <?php
  2. namespace Qcloud\Cos;
  3. use GuzzleHttp\Client as HttpClient;
  4. use GuzzleHttp\HandlerStack;
  5. use Psr\Http\Message\RequestInterface;
  6. use Psr\Http\Message\ResponseInterface;
  7. use GuzzleHttp\Command\Guzzle\Description;
  8. use GuzzleHttp\Command\Guzzle\GuzzleClient;
  9. use GuzzleHttp\Command\Guzzle\Deserializer;
  10. use GuzzleHttp\Command\CommandInterface;
  11. use GuzzleHttp\Exception\ConnectException;
  12. use GuzzleHttp\Middleware;
  13. use GuzzleHttp\Psr7;
  14. /**
  15. * @method object AbortMultipartUpload(array $args) 舍弃一个分块上传且删除已上传的分片块
  16. * @method object CreateBucket(array $args) 创建存储桶(Bucket)
  17. * @method object CompleteMultipartUpload(array $args) 完成整个分块上传
  18. * @method object CreateMultipartUpload(array $args) 初始化分块上传
  19. * @method object CopyObject(array $args) 复制对象
  20. * @method object DeleteBucket(array $args) 删除存储桶 (Bucket)
  21. * @method object DeleteBucketCors(array $args) 删除跨域访问配置信息
  22. * @method object DeleteBucketTagging(array $args) 删除存储桶标签信息
  23. * @method object DeleteBucketInventory(array $args) 删除存储桶标清单任务
  24. * @method object DeleteObject(array $args) 删除 COS 上单个对象
  25. * @method object DeleteObjects(array $args) 批量删除 COS 对象
  26. * @method object DeleteBucketWebsite(array $args) 删除存储桶(Bucket)的website
  27. * @method object DeleteBucketLifecycle(array $args) 删除存储桶(Bucket)的生命周期配置
  28. * @method object DeleteBucketReplication(array $args) 删除跨区域复制配置
  29. * @method object PutObjectTagging(array $args) 配置对象标签
  30. * @method object GetObjectTagging(array $args) 获取对象标签信息
  31. * @method object DeleteObjectTagging(array $args) 删除对象标签
  32. * @method object GetObject(array $args) 下载对象
  33. * @method object GetObjectAcl(array $args) 获取 COS 对象的访问权限信息(Access Control List, ACL)
  34. * @method object GetBucketAcl(array $args) 获取存储桶(Bucket)的访问权限信息(Access Control List, ACL)
  35. * @method object GetBucketCors(array $args) 查询存储桶(Bucket)跨域访问配置信息
  36. * @method object GetBucketDomain(array $args) 查询存储桶(Bucket)Domain配置信息
  37. * @method object GetBucketAccelerate(array $args) 查询存储桶(Bucket)Accelerate配置信息
  38. * @method object GetBucketWebsite(array $args) 查询存储桶(Bucket)Website配置信息
  39. * @method object GetBucketLifecycle(array $args) 查询存储桶(Bucket)的生命周期配置
  40. * @method object GetBucketVersioning(array $args) 获取存储桶(Bucket)版本控制信息
  41. * @method object GetBucketReplication(array $args) 获取存储桶(Bucket)跨区域复制配置信息
  42. * @method object GetBucketLocation(array $args) 获取存储桶(Bucket)所在的地域信息
  43. * @method object GetBucketNotification(array $args) 获取存储桶(Bucket)Notification信息
  44. * @method object GetBucketLogging(array $args) 获取存储桶(Bucket)日志信息
  45. * @method object GetBucketInventory(array $args) 获取存储桶(Bucket)清单信息
  46. * @method object GetBucketTagging(array $args) 获取存储桶(Bucket)标签信息
  47. * @method object UploadPart(array $args) 分块上传
  48. * @method object PutObject(array $args) 上传对象
  49. * @method object AppendObject(array $args) 追加对象
  50. * @method object PutObjectAcl(array $args) 设置 COS 对象的访问权限信息(Access Control List, ACL)
  51. * @method object PutBucketAcl(array $args) 设置存储桶(Bucket)的访问权限(Access Control List, ACL)
  52. * @method object PutBucketCors(array $args) 设置存储桶(Bucket)的跨域配置信息
  53. * @method object PutBucketDomain(array $args) 设置存储桶(Bucket)的Domain信息
  54. * @method object PutBucketLifecycle(array $args) 设置存储桶(Bucket)生命周期配置
  55. * @method object PutBucketVersioning(array $args) 存储桶(Bucket)版本控制
  56. * @method object PutBucketAccelerate(array $args) 配置存储桶(Bucket)Accelerate
  57. * @method object PutBucketWebsite(array $args) 配置存储桶(Bucket)website
  58. * @method object PutBucketReplication(array $args) 配置存储桶(Bucket)跨区域复制
  59. * @method object PutBucketNotification(array $args) 设置存储桶(Bucket)的回调设置
  60. * @method object PutBucketTagging(array $args) 配置存储桶(Bucket)标签
  61. * @method object PutBucketLogging(array $args) 开启存储桶(Bucket)日志服务
  62. * @method object PutBucketInventory(array $args) 配置存储桶(Bucket)清单
  63. * @method object RestoreObject(array $args) 回热归档对象
  64. * @method object ListParts(array $args) 查询存储桶(Bucket)中正在进行中的分块上传对象
  65. * @method object ListObjects(array $args) 查询存储桶(Bucket)下的部分或者全部对象
  66. * @method object ListBuckets 获取所属账户的所有存储空间列表
  67. * @method object ListObjectVersions(array $args) 获取多版本对象
  68. * @method object ListMultipartUploads(array $args) 获取已上传分块列表
  69. * @method object ListBucketInventoryConfigurations(array $args) 获取清单列表
  70. * @method object HeadObject(array $args) 获取对象的meta信息
  71. * @method object HeadBucket(array $args) 存储桶(Bucket)是否存在
  72. * @method object UploadPartCopy(array $args) 分块copy
  73. * @method object SelectObjectContent(array $args) 检索对象内容
  74. * @method object PutBucketIntelligentTiering(array $args) 存储桶(Bucket)开启智能分层
  75. * @method object GetBucketIntelligentTiering(array $args) 查询存储桶(Bucket)智能分层
  76. * @method object ImageInfo(array $args) 万象-获取图片基本信息
  77. * @method object ImageExif(array $args) 万象-获取图片EXIF信息
  78. * @method object ImageAve(array $args) 万象-获取图片主色调信息
  79. * @method object ImageProcess(array $args) 万象-云上数据处理
  80. * @method object Qrcode(array $args) 万象-二维码下载时识别
  81. * @method object QrcodeGenerate(array $args) 万象-二维码生成
  82. * @method object DetectLabel(array $args) 万象-图片标签
  83. * @method object PutBucketImageStyle(array $args) 万象-增加样式
  84. * @method object GetBucketImageStyle(array $args) 万象-查询样式
  85. * @method object DeleteBucketImageStyle(array $args) 万象-删除样式
  86. * @method object PutBucketGuetzli(array $args) 万象-开通Guetzli压缩
  87. * @method object GetBucketGuetzli(array $args) 万象-查询Guetzli状态
  88. * @method object DeleteBucketGuetzli(array $args) 万象-关闭Guetzli压缩
  89. * @method object GetObjectSensitiveContentRecognition(array $args) 图片审核
  90. * @method object DetectText(array $args) 文本审核
  91. * @method object GetSnapshot(array $args) 媒体截图
  92. * @method object PutBucketReferer(array $args) 添加防盗链
  93. * @method object GetBucketReferer(array $args) 获取防盗链规则
  94. * @method object GetMediaInfo(array $args) 获取媒体信息
  95. * @method object CreateMediaTranscodeJobs(array $args) 媒体转码
  96. * @method object CreateMediaSnapshotJobs(array $args) 媒体转码
  97. * @method object CreateMediaConcatJobs(array $args) 媒体截图
  98. * @method object DetectAudio(array $args) 媒体拼接
  99. * @method object GetDetectAudioResult(array $args) 音频审核
  100. * @method object GetDetectTextResult(array $args) 主动获取音频审核结果
  101. * @method object DetectVideo(array $args) 主动获取文本文件审核结果
  102. * @method object GetDetectVideoResult(array $args) 视频审核
  103. * @method object DetectDocument(array $args) 主动获取视频审核结果
  104. * @method object GetDetectDocumentResult(array $args) 文档审核
  105. * @method object CreateDocProcessJobs(array $args) 主动获取文档审核结果
  106. * @method object DescribeDocProcessQueues(array $args) 提交文档转码任务
  107. * @method object DescribeDocProcessJob(array $args) 查询文档转码队列
  108. * @method object GetDescribeDocProcessJobs(array $args) 查询文档转码任务
  109. * @method object DetectImage(array $args) 图片审核
  110. * @method object DetectImages(array $args) 图片审核-批量
  111. * @see \Qcloud\Cos\Service::getService()
  112. */
  113. class Client extends GuzzleClient {
  114. const VERSION = '2.4.4';
  115. public $httpClient;
  116. private $api;
  117. private $desc;
  118. private $action;
  119. private $operation;
  120. private $cosConfig;
  121. private $signature;
  122. private $rawCosConfig;
  123. public function __construct(array $cosConfig) {
  124. $this->rawCosConfig = $cosConfig;
  125. $this->cosConfig['schema'] = isset($cosConfig['schema']) ? $cosConfig['schema'] : 'http';
  126. $this->cosConfig['region'] = isset($cosConfig['region']) ? region_map($cosConfig['region']) : null;
  127. $this->cosConfig['appId'] = isset($cosConfig['credentials']['appId']) ? $cosConfig['credentials']['appId'] : null;
  128. $this->cosConfig['secretId'] = isset($cosConfig['credentials']['secretId']) ? $cosConfig['credentials']['secretId'] : '';
  129. $this->cosConfig['secretKey'] = isset($cosConfig['credentials']['secretKey']) ? $cosConfig['credentials']['secretKey'] : '';
  130. $this->cosConfig['anonymous'] = isset($cosConfig['credentials']['anonymous']) ? $cosConfig['credentials']['anonymous'] : false;
  131. $this->cosConfig['token'] = isset($cosConfig['credentials']['token']) ? $cosConfig['credentials']['token'] : null;
  132. $this->cosConfig['timeout'] = isset($cosConfig['timeout']) ? $cosConfig['timeout'] : 3600;
  133. $this->cosConfig['connect_timeout'] = isset($cosConfig['connect_timeout']) ? $cosConfig['connect_timeout'] : 3600;
  134. $this->cosConfig['ip'] = isset($cosConfig['ip']) ? $cosConfig['ip'] : null;
  135. $this->cosConfig['port'] = isset($cosConfig['port']) ? $cosConfig['port'] : null;
  136. $this->cosConfig['endpoint'] = isset($cosConfig['endpoint']) ? $cosConfig['endpoint'] : null;
  137. $this->cosConfig['domain'] = isset($cosConfig['domain']) ? $cosConfig['domain'] : null;
  138. $this->cosConfig['proxy'] = isset($cosConfig['proxy']) ? $cosConfig['proxy'] : null;
  139. $this->cosConfig['retry'] = isset($cosConfig['retry']) ? $cosConfig['retry'] : 1;
  140. $this->cosConfig['userAgent'] = isset($cosConfig['userAgent']) ? $cosConfig['userAgent'] : 'cos-php-sdk-v5.'. Client::VERSION;
  141. $this->cosConfig['pathStyle'] = isset($cosConfig['pathStyle']) ? $cosConfig['pathStyle'] : false;
  142. $this->cosConfig['signHost'] = isset($cosConfig['signHost']) ? $cosConfig['signHost'] : true;
  143. $this->cosConfig['allow_redirects'] = isset($cosConfig['allow_redirects']) ? $cosConfig['allow_redirects'] : false;
  144. $this->cosConfig['allow_accelerate'] = isset($cosConfig['allow_accelerate']) ? $cosConfig['allow_accelerate'] : false;
  145. // check config
  146. $this->inputCheck();
  147. $service = Service::getService();
  148. $handler = HandlerStack::create();
  149. $handler->push(Middleware::retry($this->retryDecide(), $this->retryDelay()));
  150. $handler->push(Middleware::mapRequest(function (RequestInterface $request) {
  151. return $request->withHeader('User-Agent', $this->cosConfig['userAgent']);
  152. }));
  153. if ($this->cosConfig['anonymous'] != true) {
  154. $handler->push($this::handleSignature($this->cosConfig['secretId'], $this->cosConfig['secretKey'], $this->cosConfig['signHost']));
  155. }
  156. if ($this->cosConfig['token'] != null) {
  157. $handler->push(Middleware::mapRequest(function (RequestInterface $request) {
  158. return $request->withHeader('x-cos-security-token', $this->cosConfig['token']);
  159. }));
  160. }
  161. $handler->push($this::handleErrors());
  162. $this->signature = new Signature(trim($this->cosConfig['secretId']), trim($this->cosConfig['secretKey']), $this->cosConfig, trim($this->cosConfig['token'] ));
  163. $area = $this->cosConfig['allow_accelerate'] ? 'accelerate' : $this->cosConfig['region'];
  164. $this->httpClient = new HttpClient([
  165. 'base_uri' => $this->cosConfig['schema'].'://cos.' . $area . '.myqcloud.com/',
  166. 'timeout' => $this->cosConfig['timeout'],
  167. 'handler' => $handler,
  168. 'proxy' => $this->cosConfig['proxy'],
  169. 'allow_redirects' => $this->cosConfig['allow_redirects']
  170. ]);
  171. $this->desc = new Description($service);
  172. $this->api = (array)($this->desc->getOperations());
  173. parent::__construct($this->httpClient, $this->desc, [$this,
  174. 'commandToRequestTransformer'], [$this, 'responseToResultTransformer'],
  175. null);
  176. }
  177. public function inputCheck() {
  178. //检查Region
  179. if (empty($this->cosConfig['region']) &&
  180. empty($this->cosConfig['domain']) &&
  181. empty($this->cosConfig['endpoint']) &&
  182. empty($this->cosConfig['ip'])) {
  183. $e = new Exception\CosException('Region is empty');
  184. $e->setExceptionCode('Invalid Argument');
  185. throw $e;
  186. }
  187. }
  188. public function retryDecide() {
  189. return function (
  190. $retries,
  191. RequestInterface $request,
  192. ResponseInterface $response = null,
  193. \Exception $exception = null
  194. ) {
  195. if ($retries >= $this->cosConfig['retry']) {
  196. return false;
  197. }
  198. if ($response != null && $response->getStatusCode() >= 400 ) {
  199. return true;
  200. }
  201. if ($exception instanceof Exception\ServiceResponseException) {
  202. if ($exception->getStatusCode() >= 400) {
  203. return true;
  204. }
  205. }
  206. if ($exception instanceof ConnectException) {
  207. return true;
  208. }
  209. return false;
  210. };
  211. }
  212. public function retryDelay() {
  213. return function ($numberOfRetries) {
  214. return 1000 * $numberOfRetries;
  215. };
  216. }
  217. public function commandToRequestTransformer(CommandInterface $command)
  218. {
  219. $this->action = $command->GetName();
  220. $this->operation = $this->api[$this->action];
  221. $transformer = new CommandToRequestTransformer($this->cosConfig, $this->operation);
  222. $seri = new Serializer($this->desc);
  223. $request = $seri($command);
  224. $request = $transformer->bucketStyleTransformer($command, $request);
  225. $request = $transformer->uploadBodyTransformer($command, $request);
  226. $request = $transformer->metadataTransformer($command, $request);
  227. $request = $transformer->queryStringTransformer($command, $request);
  228. $request = $transformer->md5Transformer($command, $request);
  229. $request = $transformer->specialParamTransformer($command, $request);
  230. $request = $transformer->ciParamTransformer($command, $request);
  231. $request = $transformer->cosDomain2CiTransformer($command, $request);
  232. return $request;
  233. }
  234. public function responseToResultTransformer(ResponseInterface $response, RequestInterface $request, CommandInterface $command)
  235. {
  236. $transformer = new ResultTransformer($this->cosConfig, $this->operation);
  237. $transformer->writeDataToLocal($command, $request, $response);
  238. $deseri = new Deserializer($this->desc, true);
  239. $result = $deseri($response, $request, $command);
  240. $result = $transformer->metaDataTransformer($command, $response, $result);
  241. $result = $transformer->extraHeadersTransformer($command, $request, $result);
  242. $result = $transformer->selectContentTransformer($command, $result);
  243. $result = $transformer->ciContentInfoTransformer($command, $result);
  244. return $result;
  245. }
  246. public function __destruct() {
  247. }
  248. public function __call($method, array $args) {
  249. try {
  250. $rt = parent::__call(ucfirst($method), $args);
  251. return $rt;
  252. } catch (\Exception $e) {
  253. $previous = $e->getPrevious();
  254. if ($previous !== null) {
  255. throw $previous;
  256. } else {
  257. throw $e;
  258. }
  259. }
  260. }
  261. public function getApi() {
  262. return $this->api;
  263. }
  264. private function getCosConfig() {
  265. return $this->cosConfig;
  266. }
  267. private function createPresignedUrl(RequestInterface $request, $expires) {
  268. return $this->signature->createPresignedUrl($request, $expires);
  269. }
  270. public function getPresignedUrl($method, $args, $expires = "+30 minutes") {
  271. $command = $this->getCommand($method, $args);
  272. $request = $this->commandToRequestTransformer($command);
  273. return $this->createPresignedUrl($request, $expires);
  274. }
  275. public function getObjectUrl($bucket, $key, $expires = "+30 minutes", array $args = array()) {
  276. $command = $this->getCommand('GetObject', $args + array('Bucket' => $bucket, 'Key' => $key));
  277. $request = $this->commandToRequestTransformer($command);
  278. return $this->createPresignedUrl($request, $expires)->__toString();
  279. }
  280. public function getObjectUrlWithoutSign($bucket, $key, array $args = array()) {
  281. $command = $this->getCommand('GetObject', $args + array('Bucket' => $bucket, 'Key' => $key));
  282. $request = $this->commandToRequestTransformer($command);
  283. return $request->getUri()-> __toString();
  284. }
  285. public function upload($bucket, $key, $body, $options = array()) {
  286. $body = Psr7\Utils::streamFor($body);
  287. $options['Retry'] = $this->cosConfig['retry'];
  288. $options['PartSize'] = isset($options['PartSize']) ? $options['PartSize'] : MultipartUpload::DEFAULT_PART_SIZE;
  289. if ($body->getSize() < $options['PartSize']) {
  290. $rt = $this->putObject(array(
  291. 'Bucket' => $bucket,
  292. 'Key' => $key,
  293. 'Body' => $body,
  294. ) + $options);
  295. }
  296. else {
  297. $multipartUpload = new MultipartUpload($this, $body, array(
  298. 'Bucket' => $bucket,
  299. 'Key' => $key,
  300. ) + $options);
  301. $rt = $multipartUpload->performUploading();
  302. }
  303. return $rt;
  304. }
  305. public function download($bucket, $key, $saveAs, $options = array()) {
  306. $options['PartSize'] = isset($options['PartSize']) ? $options['PartSize'] : RangeDownload::DEFAULT_PART_SIZE;
  307. $contentLength = 0;
  308. $versionId = isset($options['VersionId']) ? $options['VersionId'] : '';
  309. $rt = $this->headObject(array(
  310. 'Bucket'=>$bucket,
  311. 'Key'=>$key,
  312. 'VersionId'=>$versionId,
  313. )
  314. );
  315. $contentLength = $rt['ContentLength'];
  316. $resumableJson = [
  317. 'LastModified' => $rt['LastModified'],
  318. 'ContentLength' => $rt['ContentLength'],
  319. 'ETag' => $rt['ETag'],
  320. 'Crc64ecma' => $rt['Crc64ecma']
  321. ];
  322. $options['ResumableJson'] = $resumableJson;
  323. if ($contentLength < $options['PartSize']) {
  324. $rt = $this->getObject(array(
  325. 'Bucket' => $bucket,
  326. 'Key' => $key,
  327. 'SaveAs' => $saveAs,
  328. ) + $options);
  329. } else {
  330. $rangeDownload = new RangeDownload($this, $contentLength, $saveAs, array(
  331. 'Bucket' => $bucket,
  332. 'Key' => $key,
  333. ) + $options);
  334. $rt = $rangeDownload->performDownloading();
  335. }
  336. return $rt;
  337. }
  338. public function resumeUpload($bucket, $key, $body, $uploadId, $options = array()) {
  339. $body = Psr7\Utils::streamFor($body);
  340. $options['PartSize'] = isset($options['PartSize']) ? $options['PartSize'] : MultipartUpload::DEFAULT_PART_SIZE;
  341. $multipartUpload = new MultipartUpload($this, $body, array(
  342. 'Bucket' => $bucket,
  343. 'Key' => $key,
  344. 'UploadId' => $uploadId,
  345. ) + $options);
  346. $rt = $multipartUpload->resumeUploading();
  347. return $rt;
  348. }
  349. public function copy($bucket, $key, $copySource, $options = array()) {
  350. $options['PartSize'] = isset($options['PartSize']) ? $options['PartSize'] : Copy::DEFAULT_PART_SIZE;
  351. // set copysource client
  352. $sourceConfig = $this->rawCosConfig;
  353. $sourceConfig['region'] = $copySource['Region'];
  354. $cosSourceClient = new Client($sourceConfig);
  355. $copySource['VersionId'] = isset($copySource['VersionId']) ? $copySource['VersionId'] : '';
  356. $rt = $cosSourceClient->headObject(
  357. array('Bucket'=>$copySource['Bucket'],
  358. 'Key'=>$copySource['Key'],
  359. 'VersionId'=>$copySource['VersionId'],
  360. )
  361. );
  362. $contentLength = $rt['ContentLength'];
  363. // sample copy
  364. if ($contentLength < $options['PartSize']) {
  365. $rt = $this->copyObject(array(
  366. 'Bucket' => $bucket,
  367. 'Key' => $key,
  368. 'CopySource' => "{$copySource['Bucket']}.cos.{$copySource['Region']}.myqcloud.com/{$copySource['Key']}?versionId={$copySource['VersionId']}",
  369. ) + $options
  370. );
  371. return $rt;
  372. }
  373. // multi part copy
  374. $copySource['ContentLength'] = $contentLength;
  375. $copy = new Copy($this, $copySource, array(
  376. 'Bucket' => $bucket,
  377. 'Key' => $key
  378. ) + $options
  379. );
  380. return $copy->copy();
  381. }
  382. public function doesBucketExist($bucket, array $options = array())
  383. {
  384. try {
  385. $this->HeadBucket(array(
  386. 'Bucket' => $bucket));
  387. return true;
  388. } catch (\Exception $e){
  389. return false;
  390. }
  391. }
  392. public function doesObjectExist($bucket, $key, array $options = array())
  393. {
  394. try {
  395. $this->HeadObject(array(
  396. 'Bucket' => $bucket,
  397. 'Key' => $key));
  398. return true;
  399. } catch (\Exception $e){
  400. return false;
  401. }
  402. }
  403. public static function explodeKey($key) {
  404. // Remove a leading slash if one is found
  405. $split_key = explode('/', $key && $key[0] == '/' ? substr($key, 1) : $key);
  406. // Remove empty element
  407. $split_key = array_filter($split_key, function($var) {
  408. return !($var == '' || $var == null);
  409. });
  410. $final_key = implode("/", $split_key);
  411. if (substr($key, -1) == '/') {
  412. $final_key = $final_key . '/';
  413. }
  414. return $final_key;
  415. }
  416. public static function handleSignature($secretId, $secretKey, $signHost) {
  417. return function (callable $handler) use ($secretId, $secretKey, $signHost) {
  418. return new SignatureMiddleware($handler, $secretId, $secretKey, $signHost);
  419. };
  420. }
  421. public static function handleErrors() {
  422. return function (callable $handler) {
  423. return new ExceptionMiddleware($handler);
  424. };
  425. }
  426. }