{"id":2497,"date":"2025-03-24T08:47:09","date_gmt":"2025-03-23T23:47:09","guid":{"rendered":"https:\/\/dexall.co.jp\/articles\/?p=2497"},"modified":"2025-03-24T08:47:39","modified_gmt":"2025-03-23T23:47:39","slug":"%e3%80%90%e4%bf%9d%e5%ad%98%e7%89%88%e3%80%91laravel-api%e3%81%ae%e4%bd%9c%e3%82%8a%e6%96%b9%e5%ae%8c%e5%85%a8%e3%82%ac%e3%82%a4%e3%83%892024-%e8%aa%8d%e8%a8%bc%e3%81%8b%e3%82%89%e3%83%86%e3%82%b9","status":"publish","type":"post","link":"https:\/\/dexall.co.jp\/articles\/?p=2497","title":{"rendered":"\u3010\u4fdd\u5b58\u7248\u3011Laravel API\u306e\u4f5c\u308a\u65b9\u5b8c\u5168\u30ac\u30a4\u30c92024 &#8211; \u8a8d\u8a3c\u304b\u3089\u30c6\u30b9\u30c8\u307e\u3067"},"content":{"rendered":"\n<div class=\"toc\"><br \/>\n<b>Warning<\/b>:  Undefined array key \"is_admin\" in <b>\/home\/xs392991\/dexall.co.jp\/public_html\/articles\/wp-content\/themes\/sango-theme\/library\/gutenberg\/dist\/classes\/Toc.php<\/b> on line <b>116<\/b><br \/>\n<br \/>\n<b>Warning<\/b>:  Undefined array key \"is_category_top\" in <b>\/home\/xs392991\/dexall.co.jp\/public_html\/articles\/wp-content\/themes\/sango-theme\/library\/gutenberg\/dist\/classes\/Toc.php<\/b> on line <b>121<\/b><br \/>\n<br \/>\n<b>Warning<\/b>:  Undefined array key \"is_top\" in <b>\/home\/xs392991\/dexall.co.jp\/public_html\/articles\/wp-content\/themes\/sango-theme\/library\/gutenberg\/dist\/classes\/Toc.php<\/b> on line <b>128<\/b><br \/>\n    <div id=\"toc_container\" class=\"sgb-toc--bullets js-smooth-scroll\" data-dialog-title=\"\u76ee\u6b21\">\n      <p class=\"toc_title\">\u76ee\u6b21 <\/p>\n      <ul class=\"toc_list\">  <li class=\"first\">    <a href=\"#i-0\">Laravel API\u306e\u57fa\u790e\u77e5\u8b58<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-1\">RESTful API\u3068\u306f\u4f55\u304b \u2013 Laravel\u3067\u306e\u4f4d\u7f6e\u3065\u3051<\/a>      <\/li>      <li>        <a href=\"#i-2\">\u306a\u305cLaravel\u304cAPI\u958b\u767a\u306b\u6700\u9069\u306a\u306e\u304b<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-3\">API\u958b\u767a\u306b\u5fc5\u8981\u306a\u74b0\u5883\u8a2d\u5b9a\u3068\u30d1\u30c3\u30b1\u30fc\u30b8<\/a>      <\/li>    <\/ul>  <\/li>  <li>    <a href=\"#i-8\">Laravel\u3067\u306eAPI\u8a2d\u8a08\u306e\u30d9\u30b9\u30c8\u30d7\u30e9\u30af\u30c6\u30a3\u30b9<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-9\">API\u30ea\u30bd\u30fc\u30b9\u3092\u6d3b\u7528\u3057\u305f\u52b9\u7387\u7684\u306aJSON\u30ec\u30b9\u30dd\u30f3\u30b9\u8a2d\u8a08<\/a>      <\/li>      <li>        <a href=\"#i-12\">\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u9069\u5207\u306a\u69cb\u9020\u5316<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-15\">\u30d0\u30fc\u30b8\u30e7\u30cb\u30f3\u30b0\u3068\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u8a2d\u8a08\u306e\u8003\u3048\u65b9<\/a>      <\/li>    <\/ul>  <\/li>  <li>    <a href=\"#i-19\">\u8a8d\u8a3c\u30b7\u30b9\u30c6\u30e0\u306e\u5b9f\u88c5<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-20\">Laravel Sanctum\u3092\u4f7f\u7528\u3057\u305fAPI\u8a8d\u8a3c\u306e\u5b9f\u88c5\u65b9\u6cd5<\/a>      <\/li>      <li>        <a href=\"#i-24\">\u30c8\u30fc\u30af\u30f3\u30d9\u30fc\u30b9\u8a8d\u8a3c\u306e\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8a2d\u5b9a<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-28\">\u30bd\u30fc\u30b7\u30e3\u30eb\u8a8d\u8a3c\u306e\u7d71\u5408\u65b9\u6cd5<\/a>      <\/li>    <\/ul>  <\/li>  <li>    <a href=\"#i-32\">\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u8a2d\u8a08\u3068Eloquent API Resources<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-33\">\u52b9\u7387\u7684\u306a\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30ea\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u8a08<\/a>      <\/li>      <li>        <a href=\"#i-36\">Eloquent\u30ea\u30bd\u30fc\u30b9\u306b\u3088\u308bJSON\u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u6700\u9069\u5316<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-39\">N+1\u554f\u984c\u306e\u89e3\u6c7a\u3068\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u6539\u5584<\/a>      <\/li>    <\/ul>  <\/li>  <li>    <a href=\"#i-43\">\u30a8\u30e9\u30fc\u30cf\u30f3\u30c9\u30ea\u30f3\u30b0\u3068\u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-44\">\u52b9\u679c\u7684\u306a\u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3\u30eb\u30fc\u30eb\u306e\u5b9f\u88c5<\/a>      <\/li>      <li>        <a href=\"#i-47\">\u30ab\u30b9\u30bf\u30e0\u30a8\u30e9\u30fc\u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u8a2d\u5b9a<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-50\">\u4f8b\u5916\u51e6\u7406\u306e\u4f53\u7cfb\u7684\u306a\u30a2\u30d7\u30ed\u30fc\u30c1<\/a>      <\/li>    <\/ul>  <\/li>  <li>    <a href=\"#i-54\">API\u306e\u30c6\u30b9\u30c8\u3068\u54c1\u8cea\u4fdd\u8a3c<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-55\">PHPUnit\u3092\u4f7f\u7528\u3057\u305f\u52b9\u7387\u7684\u306a\u30c6\u30b9\u30c8\u65b9\u6cd5<\/a>      <\/li>      <li>        <a href=\"#i-58\">\u30e2\u30c3\u30af\u3068\u30d5\u30a1\u30af\u30c8\u30ea\u30fc\u3092\u6d3b\u7528\u3057\u305f\u30c6\u30b9\u30c8\u30c7\u30fc\u30bf\u306e\u4f5c\u6210<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-61\">CI\u30c4\u30fc\u30eb\u3092\u4f7f\u7528\u3057\u305f\u81ea\u52d5\u30c6\u30b9\u30c8\u74b0\u5883\u306e\u69cb\u7bc9<\/a>      <\/li>    <\/ul>  <\/li>  <li>    <a href=\"#i-65\">API\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u5316\u3068\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-66\">OpenAPI\uff08Swagger\uff09\u3092\u4f7f\u7528\u3057\u305f\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u81ea\u52d5\u751f\u6210<\/a>      <\/li>      <li>        <a href=\"#i-69\">API\u30d0\u30fc\u30b8\u30e7\u30f3\u7ba1\u7406\u306e\u5b9f\u8df5\u7684\u30a2\u30d7\u30ed\u30fc\u30c1<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-72\">\u7d99\u7d9a\u7684\u306a\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u3068\u6027\u80fd\u6700\u9069\u5316<\/a>      <\/li>    <\/ul>  <\/li>  <li class=\"last\">    <a href=\"#i-76\">\u5b9f\u8df5\u7684\u306a\u30e6\u30fc\u30b9\u30b1\u30fc\u30b9\u3068\u5b9f\u88c5\u4f8b<\/a>    <ul class=\"menu_level_1\">      <li class=\"first\">        <a href=\"#i-77\">\u30d5\u30a1\u30a4\u30eb\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u6a5f\u80fd\u306e\u5b9f\u88c5\u65b9\u6cd5<\/a>      <\/li>      <li>        <a href=\"#i-79\">\u30da\u30fc\u30b8\u30cd\u30fc\u30b7\u30e7\u30f3\u3068\u691c\u7d22\u6a5f\u80fd\u306e\u5b9f\u88c5<\/a>      <\/li>      <li class=\"last\">        <a href=\"#i-82\">WebSocket\u3092\u4f7f\u7528\u3057\u305f\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u901a\u4fe1\u306e\u7d71\u5408<\/a>      <\/li>    <\/ul>  <\/li><\/ul>\n      <a href=\"#\" class=\"sgb-toc-button js-toc-button\" rel=\"nofollow\" data-open-dialog=\"true\"><i class=\"fa fa-list\"><\/i><span class=\"sgb-toc-button__text\">\u76ee\u6b21\u3078<\/span><\/a>\n    <\/div><\/div><h2 class=\"wp-block-heading\" id=\"i-0\">Laravel API\u306e\u57fa\u790e\u77e5\u8b58<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-1\">RESTful API\u3068\u306f\u4f55\u304b \u2013 Laravel\u3067\u306e\u4f4d\u7f6e\u3065\u3051<\/h3>\n\n\n\n<p>RESTful API\u306f\u3001HTTP\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u5229\u7528\u3057\u3066\u30ea\u30bd\u30fc\u30b9\u306e\u64cd\u4f5c\u3092\u884c\u3046\u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3\u30b9\u30bf\u30a4\u30eb\u3067\u3059\u3002Laravel\u306f\u3001\u3053\u306eRESTful API\u306e\u958b\u767a\u3092\u5f37\u529b\u306b\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u304a\u308a\u3001\u52b9\u7387\u7684\u306aAPI\u958b\u767a\u3092\u5b9f\u73fe\u3059\u308b\u305f\u3081\u306e\u69d8\u3005\u306a\u6a5f\u80fd\u3092\u63d0\u4f9b\u3057\u3066\u3044\u307e\u3059\u3002<\/p>\n\n\n\n<p>RESTful API\u306e\u4e3b\u306a\u7279\u5fb4\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u30ea\u30bd\u30fc\u30b9\u6307\u5411\u306eURL\u8a2d\u8a08<\/li>\n\n\n\n<li>HTTP\u30e1\u30bd\u30c3\u30c9\u306b\u3088\u308b\u64cd\u4f5c\u306e\u660e\u78ba\u5316\uff08GET, POST, PUT, DELETE\uff09<\/li>\n\n\n\n<li>\u30b9\u30c6\u30fc\u30c8\u30ec\u30b9\u306a\u901a\u4fe1<\/li>\n\n\n\n<li>JSON\u3084XML\u306b\u3088\u308b\u30c7\u30fc\u30bf\u306e\u3084\u308a\u53d6\u308a<\/li>\n<\/ul>\n\n\n\n<p>Laravel\u3067\u306eRESTful API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306e\u57fa\u672c\u4f8b\uff1a<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ routes\/api.php\nRoute::apiResource('users', UserController::class);\n\n\/\/ \u4e0a\u8a18\u306f\u4ee5\u4e0b\u306e\u30eb\u30fc\u30c8\u3092\u81ea\u52d5\u751f\u6210\u3057\u307e\u3059\nGET \/api\/users          - index()   \/\/ \u30e6\u30fc\u30b6\u30fc\u4e00\u89a7\u306e\u53d6\u5f97\nPOST \/api\/users         - store()   \/\/ \u65b0\u898f\u30e6\u30fc\u30b6\u30fc\u306e\u4f5c\u6210\nGET \/api\/users\/{id}     - show()    \/\/ \u7279\u5b9a\u30e6\u30fc\u30b6\u30fc\u306e\u53d6\u5f97\nPUT \/api\/users\/{id}     - update()  \/\/ \u30e6\u30fc\u30b6\u30fc\u60c5\u5831\u306e\u66f4\u65b0\nDELETE \/api\/users\/{id}  - destroy() \/\/ \u30e6\u30fc\u30b6\u30fc\u306e\u524a\u9664<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-2\">\u306a\u305cLaravel\u304cAPI\u958b\u767a\u306b\u6700\u9069\u306a\u306e\u304b<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u5145\u5b9f\u3057\u305f\u958b\u767a\u30c4\u30fc\u30eb\u7fa4<\/strong><\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Artisan\u30b3\u30de\u30f3\u30c9\u306b\u3088\u308b\u52b9\u7387\u7684\u306a\u958b\u767a<\/li>\n\n\n\n<li>API\u30ea\u30bd\u30fc\u30b9\u30af\u30e9\u30b9\u306b\u3088\u308b\u7c21\u6f54\u306aJSON\u30ec\u30b9\u30dd\u30f3\u30b9<\/li>\n\n\n\n<li>\u5f37\u529b\u306aORM\uff08Eloquent\uff09\u306b\u3088\u308b\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u64cd\u4f5c<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u6a5f\u80fd\u306e\u6a19\u6e96\u88c5\u5099<\/strong><\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li>CSRF\u4fdd\u8b77<\/li>\n\n\n\n<li>XSS\u5bfe\u7b56<\/li>\n\n\n\n<li>SQL\u30a4\u30f3\u30b8\u30a7\u30af\u30b7\u30e7\u30f3\u9632\u6b62<\/li>\n\n\n\n<li>Laravel Sanctum\u306b\u3088\u308b\u8a8d\u8a3c<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u9ad8\u5ea6\u306a\u6a5f\u80fd\u306e\u7c21\u5358\u306a\u5b9f\u88c5<\/strong><\/li>\n<\/ol>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ API\u30ea\u30bd\u30fc\u30b9\u306e\u4f8b\nclass UserResource extends JsonResource\n{\n    public function toArray($request)\n    {\n        return [\n            'id' =&gt; $this-&gt;id,\n            'name' =&gt; $this-&gt;name,\n            'email' =&gt; $this-&gt;email,\n            'created_at' =&gt; $this-&gt;created_at-&gt;format('Y-m-d H:i:s')\n        ];\n    }\n}\n\n\/\/ \u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u306e\u4f7f\u7528\u4f8b\npublic function show(User $user)\n{\n    return new UserResource($user);\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-3\">API\u958b\u767a\u306b\u5fc5\u8981\u306a\u74b0\u5883\u8a2d\u5b9a\u3068\u30d1\u30c3\u30b1\u30fc\u30b8<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-4\">1. \u57fa\u672c\u7684\u306a\u74b0\u5883\u8a2d\u5b9a<\/h4>\n\n\n\n<p><strong>.env<\/strong>\u30d5\u30a1\u30a4\u30eb\u306e\u91cd\u8981\u306a\u8a2d\u5b9a\uff1a<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">APP_DEBUG=false  # \u672c\u756a\u74b0\u5883\u3067\u306f\u5fc5\u305afalse\u306b\nAPI_PREFIX=api   # API\u306eURL\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\nSANCTUM_STATEFUL_DOMAINS=your-domain.com  # Sanctum\u4f7f\u7528\u6642<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-5\">2. \u5fc5\u9808\u30d1\u30c3\u30b1\u30fc\u30b8\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># API\u8a8d\u8a3c\u7528\u306eSanctum\ncomposer require laravel\/sanctum\n\n# API\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u751f\u6210\u7528\u306eSwagger\ncomposer require \"darkaonline\/l5-swagger\"\n\n# \u30af\u30ed\u30b9\u30aa\u30ea\u30b8\u30f3\u901a\u4fe1\u7528\u306eCORS\u8a2d\u5b9a\ncomposer require fruitcake\/laravel-cors<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-6\">3. API\u306e\u57fa\u672c\u8a2d\u5b9a<\/h4>\n\n\n\n<p><strong>app\/Providers\/RouteServiceProvider.php<\/strong>\u3067\u306eAPI\u8a2d\u5b9a\uff1a<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public function boot()\n{\n    Route::prefix('api')\n         -&gt;middleware('api')\n         -&gt;namespace($this-&gt;namespace)\n         -&gt;group(base_path('routes\/api.php'));\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-7\">4. \u30ec\u30b9\u30dd\u30f3\u30b9\u30d8\u30c3\u30c0\u30fc\u306e\u8a2d\u5b9a<\/h4>\n\n\n\n<p><strong>app\/Http\/Middleware\/SetApiHeaders.php<\/strong>\u306e\u4f5c\u6210\uff1a<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">namespace App\\Http\\Middleware;\n\nclass SetApiHeaders\n{\n    public function handle($request, Closure $next)\n    {\n        $response = $next($request);\n\n        return $response-&gt;withHeaders([\n            'Content-Type' =&gt; 'application\/json',\n            'Accept' =&gt; 'application\/json',\n            'X-API-Version' =&gt; '1.0'\n        ]);\n    }\n}<\/pre>\n\n\n\n<p>\u3053\u306e\u57fa\u672c\u8a2d\u5b9a\u306b\u3088\u308a\u3001\u30bb\u30ad\u30e5\u30a2\u3067\u4fdd\u5b88\u6027\u306e\u9ad8\u3044API\u958b\u767a\u306e\u571f\u53f0\u304c\u6574\u3044\u307e\u3059\u3002\u6b21\u306e\u30bb\u30af\u30b7\u30e7\u30f3\u3067\u306f\u3001\u3053\u308c\u3089\u306e\u57fa\u790e\u77e5\u8b58\u3092\u6d3b\u7528\u3057\u305f\u5b9f\u8df5\u7684\u306aAPI\u8a2d\u8a08\u306e\u30d9\u30b9\u30c8\u30d7\u30e9\u30af\u30c6\u30a3\u30b9\u306b\u3064\u3044\u3066\u8a73\u3057\u304f\u89e3\u8aac\u3057\u3066\u3044\u304d\u307e\u3059\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"i-8\">Laravel\u3067\u306eAPI\u8a2d\u8a08\u306e\u30d9\u30b9\u30c8\u30d7\u30e9\u30af\u30c6\u30a3\u30b9<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-9\">API\u30ea\u30bd\u30fc\u30b9\u3092\u6d3b\u7528\u3057\u305f\u52b9\u7387\u7684\u306aJSON\u30ec\u30b9\u30dd\u30f3\u30b9\u8a2d\u8a08<\/h3>\n\n\n\n<p>API\u30ea\u30bd\u30fc\u30b9\u306f\u3001\u30e2\u30c7\u30eb\u30c7\u30fc\u30bf\u3092JSON\u5f62\u5f0f\u306b\u5909\u63db\u3059\u308b\u969b\u306e\u4e00\u8cab\u6027\u3068\u4fdd\u5b88\u6027\u3092\u9ad8\u3081\u308b\u305f\u3081\u306e\u91cd\u8981\u306a\u6a5f\u80fd\u3067\u3059\u3002\u4ee5\u4e0b\u306b\u3001\u52b9\u679c\u7684\u306a\u5b9f\u88c5\u30d1\u30bf\u30fc\u30f3\u3092\u793a\u3057\u307e\u3059\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-10\">1. \u57fa\u672c\u7684\u306a\u30ea\u30bd\u30fc\u30b9\u8a2d\u8a08<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Resources\/UserResource.php\nclass UserResource extends JsonResource\n{\n    public function toArray($request): array\n    {\n        return [\n            'id' =&gt; $this-&gt;id,\n            'name' =&gt; $this-&gt;name,\n            'email' =&gt; $this-&gt;email,\n            \/\/ \u6761\u4ef6\u4ed8\u304d\u306e\u5c5e\u6027\n            'is_admin' =&gt; $this-&gt;when($this-&gt;isAdmin(), true),\n            \/\/ \u30ea\u30ec\u30fc\u30b7\u30e7\u30f3\n            'posts' =&gt; PostResource::collection($this-&gt;whenLoaded('posts')),\n            \/\/ \u8a08\u7b97\u3055\u308c\u305f\u5c5e\u6027\n            'full_profile_url' =&gt; url(\"\/users\/{$this-&gt;id}\/profile\"),\n        ];\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-11\">2. \u30b3\u30ec\u30af\u30b7\u30e7\u30f3\u30ea\u30bd\u30fc\u30b9\u306e\u30ab\u30b9\u30bf\u30de\u30a4\u30ba<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Resources\/UserCollection.php\nclass UserCollection extends ResourceCollection\n{\n    public function toArray($request): array\n    {\n        return [\n            'data' =&gt; $this-&gt;collection,\n            'meta' =&gt; [\n                'total_users' =&gt; $this-&gt;collection-&gt;count(),\n                'version' =&gt; '1.0',\n            ],\n        ];\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-12\">\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u9069\u5207\u306a\u69cb\u9020\u5316<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-13\">1. \u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306e\u4f53\u7cfb\u7684\u306a\u6574\u7406<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ routes\/api.php\nRoute::prefix('v1')-&gt;group(function () {\n    \/\/ \u8a8d\u8a3c\u304c\u5fc5\u8981\u306a\u30eb\u30fc\u30c8\n    Route::middleware('auth:sanctum')-&gt;group(function () {\n        \/\/ \u30e6\u30fc\u30b6\u30fc\u95a2\u9023\n        Route::apiResource('users', UserController::class);\n        Route::apiResource('users.posts', UserPostController::class);\n\n        \/\/ \u7ba1\u7406\u8005\u5c02\u7528\n        Route::prefix('admin')-&gt;middleware('admin')-&gt;group(function () {\n            Route::apiResource('stats', StatsController::class);\n        });\n    });\n\n    \/\/ \u8a8d\u8a3c\u4e0d\u8981\u306a\u30d1\u30d6\u30ea\u30c3\u30afAPI\n    Route::get('health', [HealthController::class, 'check']);\n    Route::get('docs', [DocsController::class, 'index']);\n});<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-14\">2. \u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u69cb\u9020\u5316\u30d1\u30bf\u30fc\u30f3<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/V1\/UserController.php\nclass UserController extends Controller\n{\n    private UserService $userService;\n\n    public function __construct(UserService $userService)\n    {\n        $this-&gt;userService = $userService;\n    }\n\n    public function index(UserIndexRequest $request)\n    {\n        $users = $this-&gt;userService-&gt;listUsers($request-&gt;validated());\n        return new UserCollection($users);\n    }\n\n    public function store(UserStoreRequest $request)\n    {\n        $user = $this-&gt;userService-&gt;createUser($request-&gt;validated());\n        return new UserResource($user);\n    }\n\n    \/\/ \u696d\u52d9\u30ed\u30b8\u30c3\u30af\u306fService\u30af\u30e9\u30b9\u306b\u59d4\u8b72\n    public function update(UserUpdateRequest $request, User $user)\n    {\n        $updatedUser = $this-&gt;userService-&gt;updateUser($user, $request-&gt;validated());\n        return new UserResource($updatedUser);\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-15\">\u30d0\u30fc\u30b8\u30e7\u30cb\u30f3\u30b0\u3068\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u8a2d\u8a08\u306e\u8003\u3048\u65b9<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-16\">1. API\u30d0\u30fc\u30b8\u30e7\u30cb\u30f3\u30b0\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Providers\/RouteServiceProvider.php\npublic function boot()\n{\n    Route::prefix('api')\n         -&gt;middleware('api')\n         -&gt;group(function () {\n             \/\/ v1\u306e\u30eb\u30fc\u30c8\n             Route::prefix('v1')\n                  -&gt;middleware('api.v1')\n                  -&gt;group(base_path('routes\/api_v1.php'));\n\n             \/\/ v2\u306e\u30eb\u30fc\u30c8\uff08\u65b0\u6a5f\u80fd\u3084Breaking Changes\uff09\n             Route::prefix('v2')\n                  -&gt;middleware('api.v2')\n                  -&gt;group(base_path('routes\/api_v2.php'));\n         });\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-17\">2. \u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u547d\u540d\u898f\u5247<\/h4>\n\n\n\n<p>RESTful\u306a\u547d\u540d\u898f\u5247\u306e\u4f8b\uff1a<\/p>\n\n\n<div id=\"id-a2dbde59-b251-4e9e-946c-f626222b9860\">\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u30a2\u30af\u30b7\u30e7\u30f3<\/th><th>HTTP\u30e1\u30bd\u30c3\u30c9<\/th><th>\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8<\/th><th>\u8aac\u660e<\/th><\/tr><\/thead><tbody><tr><td>\u4e00\u89a7\u53d6\u5f97<\/td><td>GET<\/td><td>\/api\/v1\/users<\/td><td>\u30e6\u30fc\u30b6\u30fc\u4e00\u89a7\u3092\u53d6\u5f97<\/td><\/tr><tr><td>\u8a73\u7d30\u53d6\u5f97<\/td><td>GET<\/td><td>\/api\/v1\/users\/{id}<\/td><td>\u7279\u5b9a\u30e6\u30fc\u30b6\u30fc\u306e\u8a73\u7d30\u3092\u53d6\u5f97<\/td><\/tr><tr><td>\u4f5c\u6210<\/td><td>POST<\/td><td>\/api\/v1\/users<\/td><td>\u65b0\u898f\u30e6\u30fc\u30b6\u30fc\u3092\u4f5c\u6210<\/td><\/tr><tr><td>\u66f4\u65b0<\/td><td>PUT<\/td><td>\/api\/v1\/users\/{id}<\/td><td>\u30e6\u30fc\u30b6\u30fc\u60c5\u5831\u3092\u66f4\u65b0<\/td><\/tr><tr><td>\u90e8\u5206\u66f4\u65b0<\/td><td>PATCH<\/td><td>\/api\/v1\/users\/{id}<\/td><td>\u7279\u5b9a\u30d5\u30a3\u30fc\u30eb\u30c9\u306e\u307f\u66f4\u65b0<\/td><\/tr><tr><td>\u524a\u9664<\/td><td>DELETE<\/td><td>\/api\/v1\/users\/{id}<\/td><td>\u30e6\u30fc\u30b6\u30fc\u3092\u524a\u9664<\/td><\/tr><tr><td>\u30ea\u30ec\u30fc\u30b7\u30e7\u30f3<\/td><td>GET<\/td><td>\/api\/v1\/users\/{id}\/posts<\/td><td>\u30e6\u30fc\u30b6\u30fc\u306e\u6295\u7a3f\u4e00\u89a7\u3092\u53d6\u5f97<\/td><\/tr><\/tbody><\/table><\/figure>\n<\/div>\n\n\n<h4 class=\"wp-block-heading\" id=\"i-18\">3. \u30ec\u30b9\u30dd\u30f3\u30b9\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u306e\u6a19\u6e96\u5316<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Traits\/ApiResponse.php\ntrait ApiResponse\n{\n    protected function successResponse($data, $message = null, $code = 200)\n    {\n        return response()-&gt;json([\n            'status' =&gt; 'success',\n            'message' =&gt; $message,\n            'data' =&gt; $data\n        ], $code);\n    }\n\n    protected function errorResponse($message, $code)\n    {\n        return response()-&gt;json([\n            'status' =&gt; 'error',\n            'message' =&gt; $message,\n            'data' =&gt; null\n        ], $code);\n    }\n}<\/pre>\n\n\n\n<p>\u3053\u308c\u3089\u306e\u30d9\u30b9\u30c8\u30d7\u30e9\u30af\u30c6\u30a3\u30b9\u3092\u9069\u7528\u3059\u308b\u3053\u3068\u3067\u3001\u4fdd\u5b88\u6027\u304c\u9ad8\u304f\u3001\u30b9\u30b1\u30fc\u30e9\u30d6\u30eb\u306aAPI\u3092\u69cb\u7bc9\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u6b21\u306e\u30bb\u30af\u30b7\u30e7\u30f3\u3067\u306f\u3001\u3053\u308c\u3089\u306e\u8a2d\u8a08\u3092\u30d9\u30fc\u30b9\u306b\u3057\u305f\u8a8d\u8a3c\u30b7\u30b9\u30c6\u30e0\u306e\u5b9f\u88c5\u306b\u3064\u3044\u3066\u8a73\u3057\u304f\u89e3\u8aac\u3057\u3066\u3044\u304d\u307e\u3059\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"i-19\">\u8a8d\u8a3c\u30b7\u30b9\u30c6\u30e0\u306e\u5b9f\u88c5<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-20\">Laravel Sanctum\u3092\u4f7f\u7528\u3057\u305fAPI\u8a8d\u8a3c\u306e\u5b9f\u88c5\u65b9\u6cd5<\/h3>\n\n\n\n<p>Laravel Sanctum\u306f\u3001SPA\u3084\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u305f\u3081\u306e\u8efd\u91cf\u306a\u8a8d\u8a3c\u30b7\u30b9\u30c6\u30e0\u3092\u63d0\u4f9b\u3057\u307e\u3059\u3002\u4ee5\u4e0b\u306b\u3001\u6bb5\u968e\u7684\u306a\u5b9f\u88c5\u624b\u9806\u3092\u793a\u3057\u307e\u3059\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-21\">1. Sanctum\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3068\u521d\u671f\u8a2d\u5b9a<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># Sanctum\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\ncomposer require laravel\/sanctum\n\n# \u30de\u30a4\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u30d5\u30a1\u30a4\u30eb\u306e\u516c\u958b\nphp artisan vendor:publish --provider=\"Laravel\\Sanctum\\SanctumServiceProvider\"\n\n# \u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30de\u30a4\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u5b9f\u884c\nphp artisan migrate<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-22\">2. \u30e2\u30c7\u30eb\u306e\u6e96\u5099<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Models\/User.php\nuse Laravel\\Sanctum\\HasApiTokens;\n\nclass User extends Authenticatable\n{\n    use HasApiTokens, HasFactory, Notifiable;\n\n    protected $hidden = [\n        'password',\n        'remember_token',\n    ];\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-23\">3. \u8a8d\u8a3c\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/Auth\/LoginController.php\nclass LoginController extends Controller\n{\n    public function login(Request $request)\n    {\n        $request-&gt;validate([\n            'email' =&gt; 'required|email',\n            'password' =&gt; 'required',\n            'device_name' =&gt; 'required',\n        ]);\n\n        $user = User::where('email', $request-&gt;email)-&gt;first();\n\n        if (! $user || ! Hash::check($request-&gt;password, $user-&gt;password)) {\n            throw ValidationException::withMessages([\n                'email' =&gt; ['\u8a8d\u8a3c\u60c5\u5831\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002'],\n            ]);\n        }\n\n        \/\/ \u30c7\u30d0\u30a4\u30b9\u540d\u3092\u30c8\u30fc\u30af\u30f3\u540d\u3068\u3057\u3066\u4f7f\u7528\n        $token = $user-&gt;createToken($request-&gt;device_name);\n\n        return response()-&gt;json([\n            'token' =&gt; $token-&gt;plainTextToken,\n            'user' =&gt; new UserResource($user),\n        ]);\n    }\n\n    public function logout(Request $request)\n    {\n        \/\/ \u73fe\u5728\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u30c8\u30fc\u30af\u30f3\u3092\u524a\u9664\n        $request-&gt;user()-&gt;currentAccessToken()-&gt;delete();\n\n        return response()-&gt;json(['message' =&gt; '\u30ed\u30b0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002']);\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-24\">\u30c8\u30fc\u30af\u30f3\u30d9\u30fc\u30b9\u8a8d\u8a3c\u306e\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8a2d\u5b9a<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-25\">1. \u30c8\u30fc\u30af\u30f3\u306e\u6709\u52b9\u671f\u9650\u8a2d\u5b9a<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ config\/sanctum.php\nreturn [\n    'expiration' =&gt; 60 * 24, \/\/ 24\u6642\u9593\u3067\u30c8\u30fc\u30af\u30f3\u5931\u52b9\n    'token_prefix' =&gt; env('SANCTUM_TOKEN_PREFIX', 'dexall_'),\n\n    \/\/ \u30b9\u30c6\u30fc\u30c8\u30d5\u30eb\u306a\u8a8d\u8a3c\u3092\u8a31\u53ef\u3059\u308b\u30c9\u30e1\u30a4\u30f3\n    'stateful' =&gt; explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(\n        '%s%s',\n        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',\n        env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''\n    ))),\n];<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-26\">2. \u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30df\u30c9\u30eb\u30a6\u30a7\u30a2\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Middleware\/EnsureTokenIsValid.php\nclass EnsureTokenIsValid\n{\n    public function handle($request, Closure $next)\n    {\n        if (! $request-&gt;user() || \n            ! $request-&gt;user()-&gt;tokenCan('api:access')) {\n            return response()-&gt;json([\n                'message' =&gt; '\u4e0d\u6b63\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3067\u3059\u3002'\n            ], 403);\n        }\n\n        return $next($request);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-27\">3. \u30ec\u30fc\u30c8\u5236\u9650\u306e\u8a2d\u5b9a<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ routes\/api.php\nRoute::middleware(['auth:sanctum', 'throttle:api'])\n    -&gt;group(function () {\n        \/\/ \u30ec\u30fc\u30c8\u5236\u9650\u4ed8\u304d\u306e\u30eb\u30fc\u30c8\n    });\n\n\/\/ app\/Providers\/RouteServiceProvider.php\nprotected function configureRateLimiting()\n{\n    RateLimiter::for('api', function (Request $request) {\n        return Limit::perMinute(60)-&gt;by($request-&gt;user()?-&gt;id ?: $request-&gt;ip());\n    });\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-28\">\u30bd\u30fc\u30b7\u30e3\u30eb\u8a8d\u8a3c\u306e\u7d71\u5408\u65b9\u6cd5<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-29\">1. Socialite\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3068\u8a2d\u5b9a<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">composer require laravel\/socialite<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ config\/services.php\nreturn [\n    'google' =&gt; [\n        'client_id' =&gt; env('GOOGLE_CLIENT_ID'),\n        'client_secret' =&gt; env('GOOGLE_CLIENT_SECRET'),\n        'redirect' =&gt; env('GOOGLE_REDIRECT_URI'),\n    ],\n];<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-30\">2. \u30bd\u30fc\u30b7\u30e3\u30eb\u30ed\u30b0\u30a4\u30f3\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/Auth\/SocialiteController.php\nclass SocialiteController extends Controller\n{\n    public function redirectToProvider($provider)\n    {\n        return Socialite::driver($provider)-&gt;stateless()-&gt;redirect();\n    }\n\n    public function handleProviderCallback($provider)\n    {\n        try {\n            $socialUser = Socialite::driver($provider)-&gt;stateless()-&gt;user();\n\n            $user = User::firstOrCreate(\n                ['email' =&gt; $socialUser-&gt;getEmail()],\n                [\n                    'name' =&gt; $socialUser-&gt;getName(),\n                    'password' =&gt; Hash::make(Str::random(16)),\n                    'provider' =&gt; $provider,\n                    'provider_id' =&gt; $socialUser-&gt;getId(),\n                ]\n            );\n\n            $token = $user-&gt;createToken('socialite');\n\n            return response()-&gt;json([\n                'token' =&gt; $token-&gt;plainTextToken,\n                'user' =&gt; new UserResource($user),\n            ]);\n        } catch (\\Exception $e) {\n            return response()-&gt;json([\n                'message' =&gt; '\u30bd\u30fc\u30b7\u30e3\u30eb\u30ed\u30b0\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002'\n            ], 422);\n        }\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-31\">3. \u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u5f37\u5316\u306e\u305f\u3081\u306e\u30d9\u30b9\u30c8\u30d7\u30e9\u30af\u30c6\u30a3\u30b9<\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u30c8\u30fc\u30af\u30f3\u306e\u4fdd\u8b77<\/strong><\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li>HTTPS\u306e\u5f37\u5236\u4f7f\u7528<\/li>\n\n\n\n<li>\u30bb\u30ad\u30e5\u30a2\u306a\u30af\u30c3\u30ad\u30fc\u8a2d\u5b9a<\/li>\n\n\n\n<li>XSS\u304a\u3088\u3073CSRF\u5bfe\u7b56<\/li>\n<\/ul>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ config\/sanctum.php\n'middleware' =&gt; [\n    'verify_csrf_token' =&gt; App\\Http\\Middleware\\VerifyCsrfToken::class,\n    'encrypt_cookies' =&gt; App\\Http\\Middleware\\EncryptCookies::class,\n],<\/pre>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li><strong>\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u306e\u5b9f\u88c5<\/strong><\/li>\n<\/ol>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ routes\/api.php\nRoute::middleware(['auth:sanctum', 'ability:check-status'])-&gt;get('\/status', function () {\n    return response()-&gt;json(['status' =&gt; 'OK']);\n});<\/pre>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li><strong>\u30c8\u30fc\u30af\u30f3\u306e\u6a29\u9650\u7ba1\u7406<\/strong><\/li>\n<\/ol>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ \u30c8\u30fc\u30af\u30f3\u4f5c\u6210\u6642\u306b\u6a29\u9650\u3092\u6307\u5b9a\n$token = $user-&gt;createToken('api-token', ['view-stats', 'create-posts']);\n\n\/\/ \u7279\u5b9a\u306e\u6a29\u9650\u3092\u6301\u3064\u30c8\u30fc\u30af\u30f3\u306e\u307f\u30a2\u30af\u30bb\u30b9\u53ef\u80fd\nif ($request-&gt;user()-&gt;tokenCan('view-stats')) {\n    \/\/ \u30a2\u30af\u30bb\u30b9\u8a31\u53ef\n}<\/pre>\n\n\n\n<p>\u3053\u308c\u3089\u306e\u5b9f\u88c5\u306b\u3088\u308a\u3001\u30bb\u30ad\u30e5\u30a2\u3067\u67d4\u8edf\u306a\u8a8d\u8a3c\u30b7\u30b9\u30c6\u30e0\u3092\u69cb\u7bc9\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u6b21\u306e\u30bb\u30af\u30b7\u30e7\u30f3\u3067\u306f\u3001\u8a8d\u8a3c\u6e08\u307f\u30e6\u30fc\u30b6\u30fc\u306e\u30c7\u30fc\u30bf\u3092\u6271\u3046\u305f\u3081\u306e\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u8a2d\u8a08\u3068Eloquent API Resources\u306b\u3064\u3044\u3066\u89e3\u8aac\u3057\u3066\u3044\u304d\u307e\u3059\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"i-32\">\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u8a2d\u8a08\u3068Eloquent API Resources<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-33\">\u52b9\u7387\u7684\u306a\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30ea\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u8a08<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-34\">1. \u30de\u30a4\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3068\u30ea\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u57fa\u672c\u8a2d\u8a08<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ database\/migrations\/2024_02_01_000001_create_posts_table.php\npublic function up()\n{\n    Schema::create('posts', function (Blueprint $table) {\n        $table-&gt;id();\n        $table-&gt;foreignId('user_id')-&gt;constrained()-&gt;onDelete('cascade');\n        $table-&gt;string('title');\n        $table-&gt;text('content');\n        $table-&gt;string('status')-&gt;default('draft');\n        $table-&gt;timestamp('published_at')-&gt;nullable();\n        $table-&gt;timestamps();\n\n        \/\/ \u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306e\u8ffd\u52a0\n        $table-&gt;index(['status', 'published_at']);\n        $table-&gt;index('user_id');\n    });\n}\n\n\/\/ database\/migrations\/2024_02_01_000002_create_comments_table.php\npublic function up()\n{\n    Schema::create('comments', function (Blueprint $table) {\n        $table-&gt;id();\n        $table-&gt;foreignId('post_id')-&gt;constrained()-&gt;onDelete('cascade');\n        $table-&gt;foreignId('user_id')-&gt;constrained()-&gt;onDelete('cascade');\n        $table-&gt;text('content');\n        $table-&gt;timestamps();\n\n        \/\/ \u8907\u5408\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\n        $table-&gt;index(['post_id', 'created_at']);\n    });\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-35\">2. \u30e2\u30c7\u30eb\u30ea\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Models\/User.php\nclass User extends Authenticatable\n{\n    use HasApiTokens, HasFactory, Notifiable;\n\n    public function posts()\n    {\n        return $this-&gt;hasMany(Post::class);\n    }\n\n    public function comments()\n    {\n        return $this-&gt;hasMany(Comment::class);\n    }\n\n    \/\/ \u3088\u304f\u4f7f\u7528\u3055\u308c\u308b\u6761\u4ef6\u3092\u30ed\u30fc\u30ab\u30eb\u30b9\u30b3\u30fc\u30d7\u3068\u3057\u3066\u5b9a\u7fa9\n    public function scopeActive($query)\n    {\n        return $query-&gt;where('status', 'active');\n    }\n}\n\n\/\/ app\/Models\/Post.php\nclass Post extends Model\n{\n    protected $casts = [\n        'published_at' =&gt; 'datetime',\n    ];\n\n    public function user()\n    {\n        return $this-&gt;belongsTo(User::class);\n    }\n\n    public function comments()\n    {\n        return $this-&gt;hasMany(Comment::class);\n    }\n\n    \/\/ \u6295\u7a3f\u306e\u30b9\u30c6\u30fc\u30bf\u30b9\u3092\u7ba1\u7406\u3059\u308b\u5217\u6319\u578b\n    public function status(): Attribute\n    {\n        return Attribute::make(\n            get: fn (string $value) =&gt; PostStatus::from($value),\n            set: fn (PostStatus $status) =&gt; $status-&gt;value\n        );\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-36\">Eloquent\u30ea\u30bd\u30fc\u30b9\u306b\u3088\u308bJSON\u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u6700\u9069\u5316<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-37\">1. \u57fa\u672c\u7684\u306a\u30ea\u30bd\u30fc\u30b9\u69cb\u9020<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Resources\/PostResource.php\nclass PostResource extends JsonResource\n{\n    public function toArray($request): array\n    {\n        return [\n            'id' =&gt; $this-&gt;id,\n            'title' =&gt; $this-&gt;title,\n            'content' =&gt; $this-&gt;content,\n            'status' =&gt; $this-&gt;status-&gt;value,\n            'published_at' =&gt; $this-&gt;published_at?-&gt;format('Y-m-d H:i:s'),\n            'created_at' =&gt; $this-&gt;created_at-&gt;format('Y-m-d H:i:s'),\n\n            \/\/ \u6761\u4ef6\u4ed8\u304d\u3067\u542b\u3081\u308b\u30ea\u30ec\u30fc\u30b7\u30e7\u30f3\n            'user' =&gt; new UserResource($this-&gt;whenLoaded('user')),\n            'comments_count' =&gt; $this-&gt;when(\n                $request-&gt;include_counts, \n                fn() =&gt; $this-&gt;comments()-&gt;count()\n            ),\n\n            \/\/ \u30ab\u30b9\u30bf\u30e0\u5c5e\u6027\n            'excerpt' =&gt; Str::limit($this-&gt;content, 100),\n\n            \/\/ \u6a29\u9650\u306b\u57fa\u3065\u304f\u6761\u4ef6\u4ed8\u304d\u30c7\u30fc\u30bf\n            'edit_url' =&gt; $this-&gt;when(\n                $request-&gt;user()?-&gt;can('update', $this),\n                fn() =&gt; route('posts.edit', $this-&gt;id)\n            ),\n        ];\n    }\n\n    \/\/ \u30ec\u30b9\u30dd\u30f3\u30b9\u306b\u30e1\u30bf\u30c7\u30fc\u30bf\u3092\u8ffd\u52a0\n    public function with($request): array\n    {\n        return [\n            'meta' =&gt; [\n                'version' =&gt; '1.0',\n                'api_status' =&gt; 'stable',\n            ],\n        ];\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-38\">2. \u30b3\u30ec\u30af\u30b7\u30e7\u30f3\u30ea\u30bd\u30fc\u30b9\u306e\u6700\u9069\u5316<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Resources\/PostCollection.php\nclass PostCollection extends ResourceCollection\n{\n    public function toArray($request): array\n    {\n        return [\n            'data' =&gt; $this-&gt;collection,\n            'meta' =&gt; [\n                'total' =&gt; $this-&gt;collection-&gt;count(),\n                'page' =&gt; $this-&gt;currentPage(),\n                'per_page' =&gt; $this-&gt;perPage(),\n                'last_page' =&gt; $this-&gt;lastPage(),\n            ],\n        ];\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-39\">N+1\u554f\u984c\u306e\u89e3\u6c7a\u3068\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u6539\u5584<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-40\">1. Eager\u30ed\u30fc\u30c7\u30a3\u30f3\u30b0\u306e\u9069\u5207\u306a\u4f7f\u7528<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/PostController.php\nclass PostController extends Controller\n{\n    public function index(Request $request)\n    {\n        $posts = Post::query()\n            -&gt;when($request-&gt;include_user, fn($q) =&gt; $q-&gt;with('user'))\n            -&gt;when($request-&gt;include_comments, fn($q) =&gt; $q-&gt;with('comments'))\n            -&gt;when(\n                $request-&gt;include_comment_counts,\n                fn($q) =&gt; $q-&gt;withCount('comments')\n            )\n            -&gt;latest()\n            -&gt;paginate();\n\n        return new PostCollection($posts);\n    }\n\n    public function show(Post $post, Request $request)\n    {\n        \/\/ \u5fc5\u8981\u306a\u30ea\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u307f\u30ed\u30fc\u30c9\n        $post-&gt;load($this-&gt;getIncludesFromRequest($request));\n\n        return new PostResource($post);\n    }\n\n    private function getIncludesFromRequest(Request $request): array\n    {\n        return array_filter([\n            $request-&gt;include_user ? 'user' : null,\n            $request-&gt;include_comments ? 'comments.user' : null,\n        ]);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-41\">2. \u30af\u30a8\u30ea\u306e\u6700\u9069\u5316\u30c6\u30af\u30cb\u30c3\u30af<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ \u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u6700\u9069\u5316\u306e\u305f\u3081\u306e\u30af\u30a8\u30ea\u30d3\u30eb\u30c0\u30fc\nclass PostQueryBuilder extends Builder\n{\n    public function wherePublished(): self\n    {\n        return $this-&gt;where('status', 'published')\n                    -&gt;where('published_at', '&lt;=', now());\n    }\n\n    public function withBasicRelations(): self\n    {\n        return $this-&gt;with(['user:id,name,email', 'comments:id,post_id,content']);\n    }\n\n    \/\/ \u5fc5\u8981\u306a\u30ab\u30e9\u30e0\u306e\u307f\u9078\u629e\n    public function selectBasicFields(): self\n    {\n        return $this-&gt;select(['id', 'title', 'user_id', 'created_at']);\n    }\n}\n\n\/\/ \u30af\u30a8\u30ea\u30b9\u30b3\u30fc\u30d7\u306e\u6d3b\u7528\u4f8b\nPost::query()\n    -&gt;wherePublished()\n    -&gt;withBasicRelations()\n    -&gt;selectBasicFields()\n    -&gt;latest()\n    -&gt;paginate();<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-42\">3. \u30ad\u30e3\u30c3\u30b7\u30e5\u306e\u52b9\u679c\u7684\u306a\u6d3b\u7528<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/PostController.php\npublic function index(Request $request)\n{\n    $cacheKey = 'posts:' . md5($request-&gt;fullUrl());\n\n    return Cache::remember($cacheKey, now()-&gt;addMinutes(5), function () use ($request) {\n        $posts = Post::query()\n            -&gt;with($this-&gt;getIncludesFromRequest($request))\n            -&gt;latest()\n            -&gt;paginate();\n\n        return new PostCollection($posts);\n    });\n}\n\n\/\/ \u30ad\u30e3\u30c3\u30b7\u30e5\u306e\u81ea\u52d5\u30af\u30ea\u30a2\nclass Post extends Model\n{\n    protected static function booted()\n    {\n        static::saved(function ($post) {\n            Cache::tags(['posts'])-&gt;flush();\n        });\n    }\n}<\/pre>\n\n\n\n<p>\u3053\u308c\u3089\u306e\u5b9f\u88c5\u306b\u3088\u308a\u3001\u52b9\u7387\u7684\u3067\u30b9\u30b1\u30fc\u30e9\u30d6\u30eb\u306aAPI\u30b7\u30b9\u30c6\u30e0\u3092\u69cb\u7bc9\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u6b21\u306e\u30bb\u30af\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a8\u30e9\u30fc\u30cf\u30f3\u30c9\u30ea\u30f3\u30b0\u3068\u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3\u306b\u3064\u3044\u3066\u8a73\u3057\u304f\u89e3\u8aac\u3057\u3066\u3044\u304d\u307e\u3059\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"i-43\">\u30a8\u30e9\u30fc\u30cf\u30f3\u30c9\u30ea\u30f3\u30b0\u3068\u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-44\">\u52b9\u679c\u7684\u306a\u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3\u30eb\u30fc\u30eb\u306e\u5b9f\u88c5<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-45\">1. \u30d5\u30a9\u30fc\u30e0\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6d3b\u7528<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Requests\/Api\/PostStoreRequest.php\nclass PostStoreRequest extends FormRequest\n{\n    public function authorize(): bool\n    {\n        return true;\n    }\n\n    public function rules(): array\n    {\n        return [\n            'title' =&gt; ['required', 'string', 'max:255'],\n            'content' =&gt; ['required', 'string', 'min:10'],\n            'category_id' =&gt; ['required', 'exists:categories,id'],\n            'tags' =&gt; ['array', 'nullable'],\n            'tags.*' =&gt; ['exists:tags,id'],\n            'published_at' =&gt; ['nullable', 'date', 'after:today'],\n            'status' =&gt; ['required', Rule::in(['draft', 'published', 'archived'])],\n        ];\n    }\n\n    public function messages(): array\n    {\n        return [\n            'title.required' =&gt; '\u30bf\u30a4\u30c8\u30eb\u306f\u5fc5\u9808\u3067\u3059\u3002',\n            'content.min' =&gt; '\u672c\u6587\u306f\u6700\u4f4e10\u6587\u5b57\u5fc5\u8981\u3067\u3059\u3002',\n            'category_id.exists' =&gt; '\u6307\u5b9a\u3055\u308c\u305f\u30ab\u30c6\u30b4\u30ea\u30fc\u306f\u5b58\u5728\u3057\u307e\u305b\u3093\u3002',\n            'published_at.after' =&gt; '\u516c\u958b\u65e5\u306f\u660e\u65e5\u4ee5\u964d\u306e\u65e5\u4ed8\u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002',\n        ];\n    }\n\n    \/\/ \u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3\u524d\u306e\u524d\u51e6\u7406\n    protected function prepareForValidation()\n    {\n        $this-&gt;merge([\n            'slug' =&gt; Str::slug($this-&gt;title),\n        ]);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-46\">2. \u30ab\u30b9\u30bf\u30e0\u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3\u30eb\u30fc\u30eb<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Rules\/StrongPassword.php\nclass StrongPassword implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, Closure $fail): void\n    {\n        if (strlen($value) &lt; 8) {\n            $fail('\u30d1\u30b9\u30ef\u30fc\u30c9\u306f8\u6587\u5b57\u4ee5\u4e0a\u5fc5\u8981\u3067\u3059\u3002');\n        }\n\n        if (!preg_match('\/[A-Z]\/', $value)) {\n            $fail('\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u5927\u6587\u5b57\u3092\u542b\u3080\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002');\n        }\n\n        if (!preg_match('\/[0-9]\/', $value)) {\n            $fail('\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u6570\u5b57\u3092\u542b\u3080\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002');\n        }\n    }\n}\n\n\/\/ \u4f7f\u7528\u4f8b\npublic function rules(): array\n{\n    return [\n        'password' =&gt; ['required', new StrongPassword],\n    ];\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-47\">\u30ab\u30b9\u30bf\u30e0\u30a8\u30e9\u30fc\u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u8a2d\u5b9a<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-48\">1. API\u30ec\u30b9\u30dd\u30f3\u30b9\u30c8\u30ec\u30a4\u30c8\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Traits\/ApiResponse.php\ntrait ApiResponse\n{\n    protected function success($data, string $message = null, int $code = 200)\n    {\n        return response()-&gt;json([\n            'status' =&gt; 'success',\n            'message' =&gt; $message,\n            'data' =&gt; $data\n        ], $code);\n    }\n\n    protected function error(string $message, int $code, $errors = null)\n    {\n        $response = [\n            'status' =&gt; 'error',\n            'message' =&gt; $message,\n        ];\n\n        if (!is_null($errors)) {\n            $response['errors'] = $errors;\n        }\n\n        return response()-&gt;json($response, $code);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-49\">2. \u30a8\u30e9\u30fc\u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u6a19\u6e96\u5316<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Exceptions\/Handler.php\nclass Handler extends ExceptionHandler\n{\n    public function register(): void\n    {\n        $this-&gt;renderable(function (ValidationException $e, $request) {\n            if ($request-&gt;expectsJson()) {\n                return response()-&gt;json([\n                    'status' =&gt; 'error',\n                    'message' =&gt; '\u5165\u529b\u5185\u5bb9\u306b\u8aa4\u308a\u304c\u3042\u308a\u307e\u3059\u3002',\n                    'errors' =&gt; $e-&gt;errors(),\n                ], 422);\n            }\n        });\n\n        $this-&gt;renderable(function (ModelNotFoundException $e, $request) {\n            if ($request-&gt;expectsJson()) {\n                return response()-&gt;json([\n                    'status' =&gt; 'error',\n                    'message' =&gt; '\u6307\u5b9a\u3055\u308c\u305f\u30ea\u30bd\u30fc\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002',\n                ], 404);\n            }\n        });\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-50\">\u4f8b\u5916\u51e6\u7406\u306e\u4f53\u7cfb\u7684\u306a\u30a2\u30d7\u30ed\u30fc\u30c1<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-51\">1. \u30ab\u30b9\u30bf\u30e0\u4f8b\u5916\u30af\u30e9\u30b9\u306e\u4f5c\u6210<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Exceptions\/Api\/BusinessLogicException.php\nclass BusinessLogicException extends Exception\n{\n    protected $errors;\n\n    public function __construct(string $message, array $errors = [], int $code = 400)\n    {\n        parent::__construct($message, $code);\n        $this-&gt;errors = $errors;\n    }\n\n    public function getErrors(): array\n    {\n        return $this-&gt;errors;\n    }\n}\n\n\/\/ app\/Exceptions\/Api\/UnauthorizedAccessException.php\nclass UnauthorizedAccessException extends Exception\n{\n    public function __construct(string $message = '\u8a8d\u8a3c\u304c\u5fc5\u8981\u3067\u3059\u3002')\n    {\n        parent::__construct($message, 401);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-52\">2. \u4f8b\u5916\u30cf\u30f3\u30c9\u30e9\u30fc\u306e\u62e1\u5f35<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Exceptions\/Handler.php\nclass Handler extends ExceptionHandler\n{\n    protected $dontReport = [\n        BusinessLogicException::class,\n    ];\n\n    public function register(): void\n    {\n        \/\/ \u30d3\u30b8\u30cd\u30b9\u30ed\u30b8\u30c3\u30af\u4f8b\u5916\u306e\u30cf\u30f3\u30c9\u30ea\u30f3\u30b0\n        $this-&gt;renderable(function (BusinessLogicException $e, $request) {\n            return response()-&gt;json([\n                'status' =&gt; 'error',\n                'message' =&gt; $e-&gt;getMessage(),\n                'errors' =&gt; $e-&gt;getErrors(),\n            ], $e-&gt;getCode());\n        });\n\n        \/\/ \u8a8d\u8a3c\u95a2\u9023\u306e\u4f8b\u5916\u30cf\u30f3\u30c9\u30ea\u30f3\u30b0\n        $this-&gt;renderable(function (AuthenticationException $e, $request) {\n            return response()-&gt;json([\n                'status' =&gt; 'error',\n                'message' =&gt; '\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002',\n            ], 401);\n        });\n\n        \/\/ \u8a8d\u53ef\u95a2\u9023\u306e\u4f8b\u5916\u30cf\u30f3\u30c9\u30ea\u30f3\u30b0\n        $this-&gt;renderable(function (AuthorizationException $e, $request) {\n            return response()-&gt;json([\n                'status' =&gt; 'error',\n                'message' =&gt; '\u3053\u306e\u64cd\u4f5c\u3092\u5b9f\u884c\u3059\u308b\u6a29\u9650\u304c\u3042\u308a\u307e\u305b\u3093\u3002',\n            ], 403);\n        });\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-53\">3. \u5b9f\u8df5\u7684\u306a\u4f8b\u5916\u51e6\u7406\u306e\u4f7f\u7528\u4f8b<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Services\/PostService.php\nclass PostService\n{\n    public function createPost(array $data): Post\n    {\n        try {\n            DB::beginTransaction();\n\n            \/\/ \u6295\u7a3f\u306e\u4f5c\u6210\n            $post = Post::create($data);\n\n            \/\/ \u30bf\u30b0\u306e\u95a2\u9023\u4ed8\u3051\n            if (isset($data['tags'])) {\n                $post-&gt;tags()-&gt;sync($data['tags']);\n            }\n\n            DB::commit();\n            return $post;\n\n        } catch (\\Exception $e) {\n            DB::rollBack();\n\n            throw new BusinessLogicException(\n                '\u6295\u7a3f\u306e\u4f5c\u6210\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002',\n                ['system_error' =&gt; $e-&gt;getMessage()]\n            );\n        }\n    }\n\n    public function updatePostStatus(Post $post, string $status): Post\n    {\n        if (!in_array($status, ['draft', 'published', 'archived'])) {\n            throw new BusinessLogicException(\n                '\u7121\u52b9\u306a\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u6307\u5b9a\u3055\u308c\u307e\u3057\u305f\u3002',\n                ['status' =&gt; ['\u6307\u5b9a\u53ef\u80fd\u306a\u5024: draft, published, archived']]\n            );\n        }\n\n        if ($status === 'published' &amp;&amp; !$post-&gt;isReadyToPublish()) {\n            throw new BusinessLogicException(\n                '\u516c\u958b\u6761\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002',\n                ['requirements' =&gt; $post-&gt;getPublishRequirements()]\n            );\n        }\n\n        $post-&gt;update(['status' =&gt; $status]);\n        return $post;\n    }\n}<\/pre>\n\n\n\n<p>\u3053\u308c\u3089\u306e\u5b9f\u88c5\u306b\u3088\u308a\u3001\u30a8\u30e9\u30fc\u3092\u9069\u5207\u306b\u51e6\u7406\u3057\u3001\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u5206\u304b\u308a\u3084\u3059\u3044\u30d5\u30a3\u30fc\u30c9\u30d0\u30c3\u30af\u3092\u63d0\u4f9b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u6b21\u306e\u30bb\u30af\u30b7\u30e7\u30f3\u3067\u306f\u3001API\u306e\u30c6\u30b9\u30c8\u3068\u54c1\u8cea\u4fdd\u8a3c\u306b\u3064\u3044\u3066\u8a73\u3057\u304f\u89e3\u8aac\u3057\u3066\u3044\u304d\u307e\u3059\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"i-54\">API\u306e\u30c6\u30b9\u30c8\u3068\u54c1\u8cea\u4fdd\u8a3c<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-55\">PHPUnit\u3092\u4f7f\u7528\u3057\u305f\u52b9\u7387\u7684\u306a\u30c6\u30b9\u30c8\u65b9\u6cd5<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-56\">1. \u57fa\u672c\u7684\u306aAPI\u30c6\u30b9\u30c8\u306e\u69cb\u9020<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ tests\/Feature\/Api\/PostControllerTest.php\nclass PostControllerTest extends TestCase\n{\n    use RefreshDatabase;\n    use WithFaker;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this-&gt;user = User::factory()-&gt;create();\n        $this-&gt;actingAs($this-&gt;user);\n    }\n\n    public function test_can_get_posts_list()\n    {\n        \/\/ \u30c6\u30b9\u30c8\u30c7\u30fc\u30bf\u306e\u6e96\u5099\n        $posts = Post::factory()-&gt;count(3)-&gt;create();\n\n        \/\/ API\u547c\u3073\u51fa\u3057\n        $response = $this-&gt;getJson('\/api\/v1\/posts');\n\n        \/\/ \u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u691c\u8a3c\n        $response\n            -&gt;assertStatus(200)\n            -&gt;assertJsonStructure([\n                'data' =&gt; [\n                    '*' =&gt; [\n                        'id',\n                        'title',\n                        'content',\n                        'created_at'\n                    ]\n                ],\n                'meta' =&gt; [\n                    'current_page',\n                    'last_page',\n                    'per_page',\n                    'total'\n                ]\n            ]);\n    }\n\n    public function test_can_create_post()\n    {\n        $postData = [\n            'title' =&gt; $this-&gt;faker-&gt;sentence,\n            'content' =&gt; $this-&gt;faker-&gt;paragraphs(3, true),\n            'category_id' =&gt; Category::factory()-&gt;create()-&gt;id,\n        ];\n\n        $response = $this-&gt;postJson('\/api\/v1\/posts', $postData);\n\n        $response\n            -&gt;assertStatus(201)\n            -&gt;assertJson([\n                'data' =&gt; [\n                    'title' =&gt; $postData['title'],\n                    'content' =&gt; $postData['content'],\n                ]\n            ]);\n\n        $this-&gt;assertDatabaseHas('posts', [\n            'title' =&gt; $postData['title'],\n            'user_id' =&gt; $this-&gt;user-&gt;id,\n        ]);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-57\">2. \u30c6\u30b9\u30c8\u30b1\u30fc\u30b9\u306e\u4f53\u7cfb\u5316<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ tests\/Feature\/Api\/PostManagementTest.php\nclass PostManagementTest extends TestCase\n{\n    use RefreshDatabase;\n\n    private Post $post;\n    private User $admin;\n    private User $user;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        \/\/ \u30c6\u30b9\u30c8\u7528\u30c7\u30fc\u30bf\u306e\u6e96\u5099\n        $this-&gt;admin = User::factory()-&gt;admin()-&gt;create();\n        $this-&gt;user = User::factory()-&gt;create();\n        $this-&gt;post = Post::factory()-&gt;create(['user_id' =&gt; $this-&gt;user-&gt;id]);\n    }\n\n    \/**\n     * @test\n     * @dataProvider postStatusProvider\n     *\/\n    public function admin_can_change_post_status(string $status)\n    {\n        $this-&gt;actingAs($this-&gt;admin);\n\n        $response = $this-&gt;patchJson(\"\/api\/v1\/posts\/{$this-&gt;post-&gt;id}\/status\", [\n            'status' =&gt; $status\n        ]);\n\n        $response-&gt;assertStatus(200);\n        $this-&gt;assertEquals($status, $this-&gt;post-&gt;fresh()-&gt;status);\n    }\n\n    public function postStatusProvider(): array\n    {\n        return [\n            'draft status' =&gt; ['draft'],\n            'published status' =&gt; ['published'],\n            'archived status' =&gt; ['archived']\n        ];\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-58\">\u30e2\u30c3\u30af\u3068\u30d5\u30a1\u30af\u30c8\u30ea\u30fc\u3092\u6d3b\u7528\u3057\u305f\u30c6\u30b9\u30c8\u30c7\u30fc\u30bf\u306e\u4f5c\u6210<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-59\">1. \u30e2\u30c7\u30eb\u30d5\u30a1\u30af\u30c8\u30ea\u30fc\u306e\u9ad8\u5ea6\u306a\u6d3b\u7528<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ database\/factories\/PostFactory.php\nclass PostFactory extends Factory\n{\n    protected $model = Post::class;\n\n    public function definition(): array\n    {\n        return [\n            'title' =&gt; $this-&gt;faker-&gt;sentence,\n            'content' =&gt; $this-&gt;faker-&gt;paragraphs(3, true),\n            'user_id' =&gt; User::factory(),\n            'status' =&gt; $this-&gt;faker-&gt;randomElement(['draft', 'published', 'archived']),\n            'published_at' =&gt; $this-&gt;faker-&gt;dateTimeBetween('-1 month', '+1 month'),\n        ];\n    }\n\n    \/\/ \u30b9\u30c6\u30fc\u30bf\u30b9\u5225\u306e\u30d5\u30a1\u30af\u30c8\u30ea\u30fc\u30b9\u30c6\u30fc\u30c8\n    public function published(): self\n    {\n        return $this-&gt;state(function (array $attributes) {\n            return [\n                'status' =&gt; 'published',\n                'published_at' =&gt; now(),\n            ];\n        });\n    }\n\n    public function withComments(int $count = 3): self\n    {\n        return $this-&gt;has(Comment::factory()-&gt;count($count));\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-60\">2. \u30b5\u30fc\u30d3\u30b9\u306e\u30e2\u30c3\u30af\u5316<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ tests\/Unit\/Services\/PostServiceTest.php\nclass PostServiceTest extends TestCase\n{\n    private PostService $postService;\n    private MockInterface $repository;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n\n        \/\/ \u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30e2\u30c3\u30af\u4f5c\u6210\n        $this-&gt;repository = Mockery::mock(PostRepository::class);\n        $this-&gt;postService = new PostService($this-&gt;repository);\n    }\n\n    public function test_create_post_with_tags()\n    {\n        \/\/ \u30e2\u30c3\u30af\u306e\u671f\u5f85\u5024\u3092\u8a2d\u5b9a\n        $this-&gt;repository\n            -&gt;shouldReceive('create')\n            -&gt;once()\n            -&gt;with(Mockery::type('array'))\n            -&gt;andReturn(new Post(['id' =&gt; 1]));\n\n        $this-&gt;repository\n            -&gt;shouldReceive('attachTags')\n            -&gt;once()\n            -&gt;with(Mockery::type(Post::class), Mockery::type('array'))\n            -&gt;andReturn(true);\n\n        \/\/ \u30b5\u30fc\u30d3\u30b9\u30e1\u30bd\u30c3\u30c9\u306e\u5b9f\u884c\n        $result = $this-&gt;postService-&gt;createPost([\n            'title' =&gt; 'Test Post',\n            'content' =&gt; 'Test Content',\n            'tags' =&gt; [1, 2, 3]\n        ]);\n\n        $this-&gt;assertInstanceOf(Post::class, $result);\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-61\">CI\u30c4\u30fc\u30eb\u3092\u4f7f\u7528\u3057\u305f\u81ea\u52d5\u30c6\u30b9\u30c8\u74b0\u5883\u306e\u69cb\u7bc9<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-62\">1. GitHub Actions\u306e\u8a2d\u5b9a<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># .github\/workflows\/test.yml\nname: Laravel API Tests\n\non:\n  push:\n    branches: [ main, develop ]\n  pull_request:\n    branches: [ main, develop ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    services:\n      mysql:\n        image: mysql:8.0\n        env:\n          MYSQL_ROOT_PASSWORD: password\n          MYSQL_DATABASE: laravel_test\n        ports:\n          - 3306:3306\n        options: --health-cmd=\"mysqladmin ping\" --health-interval=10s --health-timeout=5s --health-retries=3\n\n    steps:\n    - uses: actions\/checkout@v2\n\n    - name: Setup PHP\n      uses: shivammathur\/setup-php@v2\n      with:\n        php-version: '8.2'\n        extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql\n        coverage: xdebug\n\n    - name: Copy .env\n      run: php -r \"file_exists('.env') || copy('.env.example', '.env');\"\n\n    - name: Install Dependencies\n      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist\n\n    - name: Generate key\n      run: php artisan key:generate\n\n    - name: Execute tests via PHPUnit\n      env:\n        DB_CONNECTION: mysql\n        DB_HOST: 127.0.0.1\n        DB_PORT: 3306\n        DB_DATABASE: laravel_test\n        DB_USERNAME: root\n        DB_PASSWORD: password\n      run: vendor\/bin\/phpunit --coverage-text<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-63\">2. \u30c6\u30b9\u30c8\u306e\u81ea\u52d5\u5316\u30b9\u30af\u30ea\u30d7\u30c8<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">#!\/bin\/bash\n# scripts\/run-tests.sh\n\n# \u74b0\u5883\u5909\u6570\u306e\u8a2d\u5b9a\nexport APP_ENV=testing\nexport DB_CONNECTION=sqlite\nexport DB_DATABASE=:memory:\n\n# \u30c6\u30b9\u30c8\u306e\u5b9f\u884c\nphp artisan test --parallel\n\n# \u30b3\u30fc\u30c9\u30ab\u30d0\u30ec\u30c3\u30b8\u30ec\u30dd\u30fc\u30c8\u306e\u751f\u6210\nphp artisan test --coverage-html reports\/coverage<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-64\">3. PHPUnit\u306e\u8a2d\u5b9a\u6700\u9069\u5316<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;!-- phpunit.xml --&gt;\n&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n&lt;phpunit xmlns:xsi=\"http:\/\/www.w3.org\/2001\/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\".\/vendor\/phpunit\/phpunit\/phpunit.xsd\"\n         bootstrap=\"vendor\/autoload.php\"\n         colors=\"true\"&gt;\n    &lt;testsuites&gt;\n        &lt;testsuite name=\"Unit\"&gt;\n            &lt;directory suffix=\"Test.php\"&gt;.\/tests\/Unit&lt;\/directory&gt;\n        &lt;\/testsuite&gt;\n        &lt;testsuite name=\"Feature\"&gt;\n            &lt;directory suffix=\"Test.php\"&gt;.\/tests\/Feature&lt;\/directory&gt;\n        &lt;\/testsuite&gt;\n        &lt;testsuite name=\"Api\"&gt;\n            &lt;directory suffix=\"Test.php\"&gt;.\/tests\/Feature\/Api&lt;\/directory&gt;\n        &lt;\/testsuite&gt;\n    &lt;\/testsuites&gt;\n    &lt;coverage processUncoveredFiles=\"true\"&gt;\n        &lt;include&gt;\n            &lt;directory suffix=\".php\"&gt;.\/app&lt;\/directory&gt;\n        &lt;\/include&gt;\n        &lt;exclude&gt;\n            &lt;directory&gt;.\/app\/Console&lt;\/directory&gt;\n            &lt;directory&gt;.\/app\/Exceptions&lt;\/directory&gt;\n            &lt;directory&gt;.\/app\/Providers&lt;\/directory&gt;\n        &lt;\/exclude&gt;\n    &lt;\/coverage&gt;\n    &lt;php&gt;\n        &lt;env name=\"APP_ENV\" value=\"testing\"\/&gt;\n        &lt;env name=\"BCRYPT_ROUNDS\" value=\"4\"\/&gt;\n        &lt;env name=\"CACHE_DRIVER\" value=\"array\"\/&gt;\n        &lt;env name=\"DB_CONNECTION\" value=\"sqlite\"\/&gt;\n        &lt;env name=\"DB_DATABASE\" value=\":memory:\"\/&gt;\n        &lt;env name=\"MAIL_MAILER\" value=\"array\"\/&gt;\n        &lt;env name=\"QUEUE_CONNECTION\" value=\"sync\"\/&gt;\n        &lt;env name=\"SESSION_DRIVER\" value=\"array\"\/&gt;\n        &lt;env name=\"TELESCOPE_ENABLED\" value=\"false\"\/&gt;\n    &lt;\/php&gt;\n&lt;\/phpunit&gt;<\/pre>\n\n\n\n<p>\u3053\u308c\u3089\u306e\u5b9f\u88c5\u306b\u3088\u308a\u3001\u9ad8\u54c1\u8cea\u306aAPI\u306e\u958b\u767a\u3068\u4fdd\u5b88\u304c\u53ef\u80fd\u306b\u306a\u308a\u307e\u3059\u3002\u6b21\u306e\u30bb\u30af\u30b7\u30e7\u30f3\u3067\u306f\u3001API\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u5316\u3068\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306b\u3064\u3044\u3066\u8a73\u3057\u304f\u89e3\u8aac\u3057\u3066\u3044\u304d\u307e\u3059\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"i-65\">API\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u5316\u3068\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-66\">OpenAPI\uff08Swagger\uff09\u3092\u4f7f\u7528\u3057\u305f\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u81ea\u52d5\u751f\u6210<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-67\">1. L5-Swagger \u306e\u8a2d\u5b9a\u3068\u57fa\u672c\u4f7f\u7528\u6cd5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/PostController.php\n\n\/**\n * @OA\\Info(\n *     version=\"1.0.0\",\n *     title=\"Laravel API Documentation\",\n *     description=\"Laravel API\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\",\n *     @OA\\Contact(\n *         email=\"support@example.com\"\n *     )\n * )\n *\/\n\n\/**\n * @OA\\SecurityScheme(\n *     type=\"http\",\n *     scheme=\"bearer\",\n *     bearerFormat=\"JWT\",\n *     securityScheme=\"bearerAuth\"\n * )\n *\/\nclass PostController extends Controller\n{\n    \/**\n     * \u6295\u7a3f\u4e00\u89a7\u306e\u53d6\u5f97\n     *\n     * @OA\\Get(\n     *     path=\"\/api\/v1\/posts\",\n     *     tags={\"Posts\"},\n     *     summary=\"\u6295\u7a3f\u4e00\u89a7\u3092\u53d6\u5f97\",\n     *     @OA\\Parameter(\n     *         name=\"page\",\n     *         in=\"query\",\n     *         description=\"\u30da\u30fc\u30b8\u756a\u53f7\",\n     *         required=false,\n     *         @OA\\Schema(type=\"integer\")\n     *     ),\n     *     @OA\\Parameter(\n     *         name=\"per_page\",\n     *         in=\"query\",\n     *         description=\"1\u30da\u30fc\u30b8\u3042\u305f\u308a\u306e\u4ef6\u6570\",\n     *         required=false,\n     *         @OA\\Schema(type=\"integer\")\n     *     ),\n     *     @OA\\Response(\n     *         response=200,\n     *         description=\"\u6295\u7a3f\u4e00\u89a7\u306e\u53d6\u5f97\u6210\u529f\",\n     *         @OA\\JsonContent(\n     *             @OA\\Property(property=\"data\", type=\"array\",\n     *                 @OA\\Items(ref=\"#\/components\/schemas\/Post\")\n     *             ),\n     *             @OA\\Property(property=\"meta\", ref=\"#\/components\/schemas\/PaginationMeta\")\n     *         )\n     *     ),\n     *     security={{\"bearerAuth\": {}}}\n     * )\n     *\/\n    public function index(Request $request)\n    {\n        \/\/ \u5b9f\u88c5\u5185\u5bb9\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-68\">2. \u30b9\u30ad\u30fc\u30de\u5b9a\u7fa9\u306e\u4f5c\u6210<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/OpenApi\/Schemas\/Post.php\n\n\/**\n * @OA\\Schema(\n *     schema=\"Post\",\n *     required={\"id\", \"title\", \"content\"},\n *     @OA\\Property(property=\"id\", type=\"integer\", example=1),\n *     @OA\\Property(property=\"title\", type=\"string\", example=\"\u8a18\u4e8b\u30bf\u30a4\u30c8\u30eb\"),\n *     @OA\\Property(property=\"content\", type=\"string\", example=\"\u8a18\u4e8b\u672c\u6587\"),\n *     @OA\\Property(property=\"user_id\", type=\"integer\", example=1),\n *     @OA\\Property(property=\"created_at\", type=\"string\", format=\"date-time\"),\n *     @OA\\Property(property=\"updated_at\", type=\"string\", format=\"date-time\")\n * )\n *\/\nclass Post {}\n\n\/**\n * @OA\\Schema(\n *     schema=\"PaginationMeta\",\n *     @OA\\Property(property=\"current_page\", type=\"integer\", example=1),\n *     @OA\\Property(property=\"last_page\", type=\"integer\", example=5),\n *     @OA\\Property(property=\"per_page\", type=\"integer\", example=15),\n *     @OA\\Property(property=\"total\", type=\"integer\", example=75)\n * )\n *\/\nclass PaginationMeta {}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-69\">API\u30d0\u30fc\u30b8\u30e7\u30f3\u7ba1\u7406\u306e\u5b9f\u8df5\u7684\u30a2\u30d7\u30ed\u30fc\u30c1<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-70\">1. URI\u30d9\u30fc\u30b9\u306e\u30d0\u30fc\u30b8\u30e7\u30cb\u30f3\u30b0\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Providers\/RouteServiceProvider.php\nclass RouteServiceProvider extends ServiceProvider\n{\n    public function boot()\n    {\n        Route::prefix('api')\n            -&gt;middleware('api')\n            -&gt;group(function () {\n                \/\/ v1\u306e\u30eb\u30fc\u30c8\n                Route::prefix('v1')\n                    -&gt;middleware('api.v1')\n                    -&gt;group(base_path('routes\/api_v1.php'));\n\n                \/\/ v2\u306e\u30eb\u30fc\u30c8\n                Route::prefix('v2')\n                    -&gt;middleware('api.v2')\n                    -&gt;group(base_path('routes\/api_v2.php'));\n            });\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-71\">2. \u30d0\u30fc\u30b8\u30e7\u30f3\u9593\u306e\u4e92\u63db\u6027\u7ba1\u7406<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Resources\/Api\/V2\/PostResource.php\nclass PostResource extends JsonResource\n{\n    public function toArray($request): array\n    {\n        \/\/ v1\u3068\u306e\u4e92\u63db\u6027\u3092\u4fdd\u3061\u306a\u304c\u3089\u3001\u65b0\u3057\u3044\u5c5e\u6027\u3092\u8ffd\u52a0\n        $baseArray = parent::toArray($request);\n\n        return array_merge($baseArray, [\n            'reading_time' =&gt; $this-&gt;calculateReadingTime(),\n            'share_url' =&gt; $this-&gt;getShareUrl(),\n            \/\/ \u975e\u63a8\u5968\u3068\u306a\u3063\u305f\u5c5e\u6027\u306e\u8b66\u544a\n            'view_count' =&gt; $this-&gt;when(\n                $request-&gt;header('Accept-Deprecation'),\n                fn() =&gt; $this-&gt;views,\n                fn() =&gt; $this-&gt;addDeprecationWarning('view_count')\n            ),\n        ]);\n    }\n\n    protected function addDeprecationWarning(string $field): mixed\n    {\n        header('X-Deprecated-Field: ' . $field);\n        return $this-&gt;views;\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-72\">\u7d99\u7d9a\u7684\u306a\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u3068\u6027\u80fd\u6700\u9069\u5316<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-73\">1. \u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u30e2\u30cb\u30bf\u30ea\u30f3\u30b0\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Middleware\/ApiMetricsMiddleware.php\nclass ApiMetricsMiddleware\n{\n    public function handle($request, Closure $next)\n    {\n        $startTime = microtime(true);\n\n        $response = $next($request);\n\n        $duration = microtime(true) - $startTime;\n\n        \/\/ \u30e1\u30c8\u30ea\u30af\u30b9\u306e\u8a18\u9332\n        $metrics = [\n            'endpoint' =&gt; $request-&gt;path(),\n            'method' =&gt; $request-&gt;method(),\n            'duration' =&gt; $duration,\n            'status' =&gt; $response-&gt;status(),\n            'user_id' =&gt; $request-&gt;user()?-&gt;id,\n            'timestamp' =&gt; now(),\n        ];\n\n        \/\/ Redis\u306b\u4fdd\u5b58\n        Redis::zadd('api_metrics', now()-&gt;timestamp, json_encode($metrics));\n\n        return $response;\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-74\">2. \u30ad\u30e3\u30c3\u30b7\u30e5\u6226\u7565\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Services\/CacheService.php\nclass CacheService\n{\n    public function rememberApi(string $key, $ttl, Closure $callback)\n    {\n        $cacheKey = $this-&gt;generateCacheKey($key);\n\n        return Cache::tags(['api'])-&gt;remember($cacheKey, $ttl, $callback);\n    }\n\n    public function invalidateEndpoint(string $endpoint)\n    {\n        $pattern = \"api:$endpoint:*\";\n        $keys = Redis::keys($pattern);\n\n        foreach ($keys as $key) {\n            Redis::del($key);\n        }\n    }\n\n    protected function generateCacheKey(string $key): string\n    {\n        return \"api:{$key}:\" . md5(request()-&gt;fullUrl());\n    }\n}\n\n\/\/ \u4f7f\u7528\u4f8b\nclass PostController extends Controller\n{\n    public function index(Request $request, CacheService $cache)\n    {\n        return $cache-&gt;rememberApi('posts.index', now()-&gt;addMinutes(5), function () {\n            return Post::paginate();\n        });\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-75\">3. \u6027\u80fd\u76e3\u8996\u3068\u30a2\u30e9\u30fc\u30c8\u8a2d\u5b9a<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Console\/Commands\/MonitorApiPerformance.php\nclass MonitorApiPerformance extends Command\n{\n    protected $signature = 'api:monitor';\n\n    public function handle()\n    {\n        \/\/ \u76f4\u8fd15\u5206\u9593\u306e\u30e1\u30c8\u30ea\u30af\u30b9\u3092\u5206\u6790\n        $metrics = Redis::zrangebyscore(\n            'api_metrics',\n            now()-&gt;subMinutes(5)-&gt;timestamp,\n            now()-&gt;timestamp\n        );\n\n        $slowEndpoints = collect($metrics)\n            -&gt;map(fn($m) =&gt; json_decode($m, true))\n            -&gt;groupBy('endpoint')\n            -&gt;map(function ($group) {\n                return [\n                    'avg_duration' =&gt; $group-&gt;avg('duration'),\n                    'count' =&gt; $group-&gt;count(),\n                    'error_rate' =&gt; $group-&gt;where('status', '&gt;=', 400)-&gt;count() \/ $group-&gt;count(),\n                ];\n            })\n            -&gt;filter(function ($stats) {\n                return $stats['avg_duration'] &gt; 1.0 || \/\/ 1\u79d2\u4ee5\u4e0a\n                       $stats['error_rate'] &gt; 0.05;    \/\/ \u30a8\u30e9\u30fc\u73875%\u4ee5\u4e0a\n            });\n\n        if ($slowEndpoints-&gt;isNotEmpty()) {\n            \/\/ Slack\u306a\u3069\u306b\u901a\u77e5\n            Notification::route('slack', env('SLACK_WEBHOOK_URL'))\n                -&gt;notify(new SlowApiEndpointsNotification($slowEndpoints));\n        }\n    }\n}<\/pre>\n\n\n\n<p>\u3053\u308c\u3089\u306e\u5b9f\u88c5\u306b\u3088\u308a\u3001API\u306e\u9577\u671f\u7684\u306a\u4fdd\u5b88\u6027\u3068\u5b89\u5b9a\u6027\u3092\u78ba\u4fdd\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u6b21\u306e\u30bb\u30af\u30b7\u30e7\u30f3\u3067\u306f\u3001\u5b9f\u8df5\u7684\u306a\u30e6\u30fc\u30b9\u30b1\u30fc\u30b9\u3068\u5b9f\u88c5\u4f8b\u306b\u3064\u3044\u3066\u8a73\u3057\u304f\u89e3\u8aac\u3057\u3066\u3044\u304d\u307e\u3059\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"i-76\">\u5b9f\u8df5\u7684\u306a\u30e6\u30fc\u30b9\u30b1\u30fc\u30b9\u3068\u5b9f\u88c5\u4f8b<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-77\">\u30d5\u30a1\u30a4\u30eb\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u6a5f\u80fd\u306e\u5b9f\u88c5\u65b9\u6cd5<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-78\">1. \u30bb\u30ad\u30e5\u30a2\u306a\u30d5\u30a1\u30a4\u30eb\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u51e6\u7406<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/FileUploadController.php\nclass FileUploadController extends Controller\n{\n    \/**\n     * @OA\\Post(\n     *     path=\"\/api\/v1\/upload\",\n     *     summary=\"\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\",\n     *     @OA\\RequestBody(\n     *         @OA\\MediaType(\n     *             mediaType=\"multipart\/form-data\",\n     *             @OA\\Schema(\n     *                 @OA\\Property(\n     *                     property=\"file\",\n     *                     type=\"string\",\n     *                     format=\"binary\"\n     *                 )\n     *             )\n     *         )\n     *     )\n     * )\n     *\/\n    public function store(FileUploadRequest $request)\n    {\n        try {\n            $file = $request-&gt;file('file');\n            $path = $file-&gt;hashName('uploads');\n\n            \/\/ S3\u3078\u306e\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\n            $url = Storage::disk('s3')-&gt;put($path, $file-&gt;get(), [\n                'ContentType' =&gt; $file-&gt;getMimeType(),\n                'ACL' =&gt; 'private',\n                'CacheControl' =&gt; 'max-age=31536000'\n            ]);\n\n            \/\/ \u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u8a18\u9332\n            $upload = FileUpload::create([\n                'user_id' =&gt; auth()-&gt;id(),\n                'original_name' =&gt; $file-&gt;getClientOriginalName(),\n                'mime_type' =&gt; $file-&gt;getMimeType(),\n                'size' =&gt; $file-&gt;getSize(),\n                'path' =&gt; $path,\n            ]);\n\n            \/\/ \u7f72\u540d\u4ed8\u304dURL\u751f\u6210\n            $signedUrl = Storage::disk('s3')-&gt;temporaryUrl(\n                $path,\n                now()-&gt;addMinutes(5)\n            );\n\n            return response()-&gt;json([\n                'message' =&gt; '\u30d5\u30a1\u30a4\u30eb\u306e\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u306b\u6210\u529f\u3057\u307e\u3057\u305f\u3002',\n                'data' =&gt; [\n                    'id' =&gt; $upload-&gt;id,\n                    'url' =&gt; $signedUrl,\n                ]\n            ]);\n\n        } catch (\\Exception $e) {\n            Log::error('\u30d5\u30a1\u30a4\u30eb\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u30a8\u30e9\u30fc: ' . $e-&gt;getMessage());\n            return response()-&gt;json([\n                'message' =&gt; '\u30d5\u30a1\u30a4\u30eb\u306e\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002'\n            ], 500);\n        }\n    }\n}\n\n\/\/ app\/Http\/Requests\/FileUploadRequest.php\nclass FileUploadRequest extends FormRequest\n{\n    public function rules(): array\n    {\n        return [\n            'file' =&gt; [\n                'required',\n                'file',\n                'max:10240', \/\/ 10MB\n                'mimes:jpeg,png,pdf,doc,docx',\n            ]\n        ];\n    }\n\n    protected function failedValidation(Validator $validator)\n    {\n        throw new HttpResponseException(\n            response()-&gt;json([\n                'message' =&gt; '\u30d0\u30ea\u30c7\u30fc\u30b7\u30e7\u30f3\u30a8\u30e9\u30fc',\n                'errors' =&gt; $validator-&gt;errors()\n            ], 422)\n        );\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-79\">\u30da\u30fc\u30b8\u30cd\u30fc\u30b7\u30e7\u30f3\u3068\u691c\u7d22\u6a5f\u80fd\u306e\u5b9f\u88c5<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-80\">1. \u9ad8\u5ea6\u306a\u691c\u7d22\u6a5f\u80fd\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/SearchController.php\nclass SearchController extends Controller\n{\n    private PostRepository $repository;\n\n    public function __construct(PostRepository $repository)\n    {\n        $this-&gt;repository = $repository;\n    }\n\n    public function search(SearchRequest $request)\n    {\n        $results = $this-&gt;repository-&gt;search($request-&gt;validated());\n\n        return PostResource::collection($results)\n            -&gt;additional([\n                'meta' =&gt; [\n                    'total' =&gt; $results-&gt;total(),\n                    'filters' =&gt; $request-&gt;validated(),\n                ]\n            ]);\n    }\n}\n\n\/\/ app\/Repositories\/PostRepository.php\nclass PostRepository\n{\n    public function search(array $params)\n    {\n        return Post::query()\n            -&gt;when($params['query'] ?? null, function ($query, $searchTerm) {\n                $query-&gt;where(function ($q) use ($searchTerm) {\n                    $q-&gt;where('title', 'like', \"%{$searchTerm}%\")\n                      -&gt;orWhere('content', 'like', \"%{$searchTerm}%\");\n                });\n            })\n            -&gt;when($params['category'] ?? null, function ($query, $category) {\n                $query-&gt;whereHas('category', function ($q) use ($category) {\n                    $q-&gt;where('slug', $category);\n                });\n            })\n            -&gt;when($params['tags'] ?? null, function ($query, $tags) {\n                $query-&gt;whereHas('tags', function ($q) use ($tags) {\n                    $q-&gt;whereIn('slug', explode(',', $tags));\n                });\n            })\n            -&gt;when($params['date_from'] ?? null, function ($query, $date) {\n                $query-&gt;where('created_at', '&gt;=', Carbon::parse($date));\n            })\n            -&gt;when($params['date_to'] ?? null, function ($query, $date) {\n                $query-&gt;where('created_at', '&lt;=', Carbon::parse($date));\n            })\n            -&gt;orderBy($params['sort_by'] ?? 'created_at', $params['sort_direction'] ?? 'desc')\n            -&gt;paginate($params['per_page'] ?? 15);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-81\">2. \u30ab\u30fc\u30bd\u30eb\u30d9\u30fc\u30b9\u306e\u30da\u30fc\u30b8\u30cd\u30fc\u30b7\u30e7\u30f3\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Http\/Controllers\/Api\/PostController.php\nclass PostController extends Controller\n{\n    public function index(Request $request)\n    {\n        $posts = Post::query()\n            -&gt;when($request-&gt;after, function ($query, $afterId) {\n                $query-&gt;where('id', '&gt;', $afterId);\n            })\n            -&gt;take($request-&gt;limit ?? 20)\n            -&gt;orderBy('id')\n            -&gt;get();\n\n        return response()-&gt;json([\n            'data' =&gt; PostResource::collection($posts),\n            'meta' =&gt; [\n                'has_more' =&gt; $posts-&gt;count() === ($request-&gt;limit ?? 20),\n                'next_cursor' =&gt; $posts-&gt;last()?-&gt;id,\n            ]\n        ]);\n    }\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"i-82\">WebSocket\u3092\u4f7f\u7528\u3057\u305f\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u901a\u4fe1\u306e\u7d71\u5408<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-83\">1. Laravel WebSockets\u306e\u8a2d\u5b9a<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ config\/websockets.php\nreturn [\n    'dashboard' =&gt; [\n        'port' =&gt; env('LARAVEL_WEBSOCKETS_PORT', 6001),\n    ],\n    'apps' =&gt; [\n        [\n            'id' =&gt; env('PUSHER_APP_ID'),\n            'name' =&gt; env('APP_NAME'),\n            'key' =&gt; env('PUSHER_APP_KEY'),\n            'secret' =&gt; env('PUSHER_APP_SECRET'),\n            'enable_client_messages' =&gt; false,\n            'enable_statistics' =&gt; true,\n        ],\n    ],\n];<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-84\">2. \u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30a4\u30d9\u30f3\u30c8\u306e\u5b9f\u88c5<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Events\/PostCreated.php\nclass PostCreated implements ShouldBroadcast\n{\n    use Dispatchable, InteractsWithSockets, SerializesModels;\n\n    public function __construct(\n        public Post $post\n    ) {}\n\n    public function broadcastOn(): array\n    {\n        return [\n            new PrivateChannel(\"user.{$this-&gt;post-&gt;user_id}\"),\n            new Channel('posts'),\n        ];\n    }\n\n    public function broadcastWith(): array\n    {\n        return [\n            'id' =&gt; $this-&gt;post-&gt;id,\n            'title' =&gt; $this-&gt;post-&gt;title,\n            'excerpt' =&gt; Str::limit($this-&gt;post-&gt;content, 100),\n            'author' =&gt; [\n                'id' =&gt; $this-&gt;post-&gt;user-&gt;id,\n                'name' =&gt; $this-&gt;post-&gt;user-&gt;name,\n            ],\n        ];\n    }\n}\n\n\/\/ app\/Http\/Controllers\/Api\/PostController.php\npublic function store(PostStoreRequest $request)\n{\n    $post = Post::create($request-&gt;validated());\n\n    \/\/ \u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30a4\u30d9\u30f3\u30c8\u306e\u767a\u706b\n    broadcast(new PostCreated($post))-&gt;toOthers();\n\n    return new PostResource($post);\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-85\">3. WebSocket\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a8d\u8a3c<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ routes\/channels.php\nBroadcast::channel('user.{id}', function ($user, $id) {\n    return (int) $user-&gt;id === (int) $id;\n});\n\nBroadcast::channel('posts', function ($user) {\n    return ['id' =&gt; $user-&gt;id, 'name' =&gt; $user-&gt;name];\n});\n\n\/\/ app\/Providers\/BroadcastServiceProvider.php\nclass BroadcastServiceProvider extends ServiceProvider\n{\n    public function boot()\n    {\n        Broadcast::routes(['middleware' =&gt; ['auth:sanctum']]);\n    }\n}<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"i-86\">4. WebSocket\u63a5\u7d9a\u306e\u30a8\u30e9\u30fc\u30cf\u30f3\u30c9\u30ea\u30f3\u30b0<\/h4>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ app\/Exceptions\/WebSocketHandler.php\nclass WebSocketHandler\n{\n    public function handle($connection, $exception)\n    {\n        Log::error('WebSocket Error: ' . $exception-&gt;getMessage(), [\n            'connection_id' =&gt; $connection-&gt;socketId,\n            'user_id' =&gt; $connection-&gt;user?-&gt;id,\n        ]);\n\n        $connection-&gt;send(json_encode([\n            'event' =&gt; 'error',\n            'data' =&gt; [\n                'message' =&gt; 'WebSocket\u63a5\u7d9a\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002',\n                'code' =&gt; $exception-&gt;getCode(),\n            ]\n        ]));\n    }\n}<\/pre>\n\n\n\n<p>\u3053\u308c\u3089\u306e\u5b9f\u88c5\u4f8b\u306b\u3088\u308a\u3001\u5b9f\u8df5\u7684\u306aAPI\u6a5f\u80fd\u3092\u52b9\u7387\u7684\u306b\u958b\u767a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3001\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u3001\u30b9\u30b1\u30fc\u30e9\u30d3\u30ea\u30c6\u30a3\u3092\u8003\u616e\u3057\u306a\u304c\u3089\u3001\u5fc5\u8981\u306b\u5fdc\u3058\u3066\u6a5f\u80fd\u3092\u62e1\u5f35\u3057\u3066\u3044\u304f\u3053\u3068\u304c\u91cd\u8981\u3067\u3059\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Warning: Undefined array key &#8220;is_admin&#8221; in \/home\/xs392991\/dexall.co.jp\/public_html\/articles\/wp-content\/themes\/ &#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12],"tags":[],"class_list":{"0":"post-2497","1":"post","2":"type-post","3":"status-publish","4":"format-standard","6":"category-php","7":"nothumb"},"_links":{"self":[{"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=\/wp\/v2\/posts\/2497","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2497"}],"version-history":[{"count":2,"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=\/wp\/v2\/posts\/2497\/revisions"}],"predecessor-version":[{"id":2499,"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=\/wp\/v2\/posts\/2497\/revisions\/2499"}],"wp:attachment":[{"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2497"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2497"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dexall.co.jp\/articles\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2497"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}