[{"data":1,"prerenderedAt":4634},["ShallowReactive",2],{"navigation":3,"search":178,"docs-/docs/boost/boost":4262,"docs-navigation":4579,"surround-/docs/boost/boost":4631},[4],{"title":5,"path":6,"stem":7,"children":8,"page":-1},"Docs","/docs","docs",[9,12,22,39,88,105,118,147,160,169],{"title":10,"path":6,"stem":11},"Laravel Restify - PHP REST API Framework & JSON API Generator","docs/index",{"title":13,"path":14,"stem":15,"children":16,"page":21},"Getting Started","/docs/getting-started","docs/1.getting-started",[17],{"title":18,"path":19,"stem":20},"Quick Start","/docs/getting-started/quickstart","docs/1.getting-started/1.quickstart",false,{"title":23,"path":24,"stem":25,"children":26,"page":21},"Auth","/docs/auth","docs/2.auth",[27,31,35],{"title":28,"path":29,"stem":30},"Authentication","/docs/auth/authentication","docs/2.auth/1.authentication",{"title":32,"path":33,"stem":34},"Authorization","/docs/auth/authorization","docs/2.auth/2.authorization",{"title":36,"path":37,"stem":38},"Profile Management","/docs/auth/profile","docs/2.auth/3.profile",{"title":40,"path":41,"stem":42,"children":43,"page":21},"Api","/docs/api","docs/3.api",[44,48,52,56,60,64,68,72,76,80,84],{"title":45,"path":46,"stem":47},"Basic Repositories","/docs/api/repositories-basic","docs/3.api/1.repositories-basic",{"title":49,"path":50,"stem":51},"Getters","/docs/api/getters","docs/3.api/10.getters",{"title":53,"path":54,"stem":55},"Serializer","/docs/api/serializer","docs/3.api/11.serializer",{"title":57,"path":58,"stem":59},"Repositories","/docs/api/repositories","docs/3.api/2.repositories",{"title":61,"path":62,"stem":63},"Advanced Repositories","/docs/api/repositories-advanced","docs/3.api/3.repositories-advanced",{"title":65,"path":66,"stem":67},"Repository Generation","/docs/api/repository-generation","docs/3.api/4.repository-generation",{"title":69,"path":70,"stem":71},"Fields","/docs/api/fields","docs/3.api/5.fields",{"title":73,"path":74,"stem":75},"Relations","/docs/api/relations","docs/3.api/6.relations",{"title":77,"path":78,"stem":79},"REST Methods","/docs/api/rest-methods","docs/3.api/7.rest-methods",{"title":81,"path":82,"stem":83},"Validation Methods","/docs/api/validation-methods","docs/3.api/8.validation-methods",{"title":85,"path":86,"stem":87},"Actions","/docs/api/actions","docs/3.api/9.actions",{"title":89,"path":90,"stem":91,"children":92,"page":21},"Search","/docs/search","docs/4.search",[93,97,101],{"title":94,"path":95,"stem":96},"Basic Filters","/docs/search/basic-filters","docs/4.search/1.basic-filters",{"title":98,"path":99,"stem":100},"Advanced Filters","/docs/search/advanced-filters","docs/4.search/2.advanced-filters",{"title":102,"path":103,"stem":104},"Sorting","/docs/search/sorting","docs/4.search/3.sorting",{"title":106,"path":107,"stem":108,"children":109,"page":21},"Graphql","/docs/graphql","docs/5.graphql",[110,114],{"title":111,"path":112,"stem":113},"GraphQL Overview","/docs/graphql/graphql","docs/5.graphql/1.graphql",{"title":115,"path":116,"stem":117},"Schema Generation","/docs/graphql/graphql-generation","docs/5.graphql/2.graphql-generation",{"title":119,"path":120,"stem":121,"children":122,"page":21},"Mcp","/docs/mcp","docs/6.mcp",[123,127,131,135,139,143],{"title":124,"path":125,"stem":126},"MCP Server","/docs/mcp/mcp","docs/6.mcp/1.mcp",{"title":128,"path":129,"stem":130},"MCP Repositories","/docs/mcp/repositories","docs/6.mcp/2.repositories",{"title":132,"path":133,"stem":134},"MCP Fields","/docs/mcp/fields","docs/6.mcp/3.fields",{"title":136,"path":137,"stem":138},"MCP Getters","/docs/mcp/getters","docs/6.mcp/4.getters",{"title":140,"path":141,"stem":142},"JSON Schema Converter","/docs/mcp/json-schema-converter","docs/6.mcp/5.json-schema-converter",{"title":144,"path":145,"stem":146},"MCP Actions","/docs/mcp/actions","docs/6.mcp/6.actions",{"title":148,"path":149,"stem":150,"children":151,"page":21},"Performance","/docs/performance","docs/7.performance",[152,156],{"title":153,"path":154,"stem":155},"Performance Overview","/docs/performance/performance","docs/7.performance/1.performance",{"title":157,"path":158,"stem":159},"Optimization Solutions","/docs/performance/solutions","docs/7.performance/2.solutions",{"title":161,"path":162,"stem":163,"children":164,"page":21},"Boost","/docs/boost","docs/8.boost",[165],{"title":166,"path":167,"stem":168},"Boost Package","/docs/boost/boost","docs/8.boost/1.boost",{"title":170,"path":171,"stem":172,"children":173,"page":21},"Testing","/docs/testing","docs/9.testing",[174],{"title":175,"path":176,"stem":177},"Testing Guide","/docs/testing/testing","docs/9.testing/1.testing",[179,183,189,194,200,205,210,215,220,225,230,235,240,245,250,255,260,265,270,275,280,283,288,293,298,303,308,313,317,322,327,332,337,342,347,352,357,362,367,372,377,382,387,392,397,402,407,412,417,422,427,432,437,442,447,450,455,460,465,470,475,480,485,490,495,500,505,510,515,520,525,530,535,540,545,550,555,560,565,570,575,580,585,590,595,600,605,610,615,620,625,630,635,640,643,647,652,657,662,667,672,677,682,685,689,694,699,704,709,714,718,723,728,732,737,742,747,752,757,762,767,772,777,781,786,791,794,799,804,809,814,819,824,829,835,840,845,850,855,860,865,870,875,880,885,890,895,900,905,910,915,918,923,928,933,938,941,946,950,955,960,965,970,975,980,985,990,995,1000,1005,1009,1014,1019,1024,1029,1034,1039,1044,1049,1054,1059,1064,1069,1074,1079,1084,1089,1094,1099,1104,1109,1114,1119,1124,1129,1134,1139,1144,1149,1154,1159,1163,1168,1172,1177,1182,1186,1191,1195,1200,1205,1209,1214,1219,1223,1228,1233,1238,1243,1248,1253,1258,1261,1266,1271,1276,1281,1286,1291,1296,1301,1306,1311,1316,1321,1326,1331,1336,1341,1346,1351,1356,1361,1366,1371,1376,1381,1386,1391,1396,1401,1404,1409,1413,1416,1421,1426,1431,1436,1440,1444,1449,1454,1457,1462,1467,1471,1476,1481,1486,1489,1494,1499,1504,1508,1513,1518,1523,1527,1532,1537,1542,1547,1552,1557,1562,1567,1571,1576,1581,1585,1590,1595,1600,1605,1610,1615,1620,1624,1629,1634,1639,1642,1647,1652,1657,1662,1667,1671,1676,1681,1686,1691,1696,1701,1706,1711,1715,1720,1725,1730,1735,1740,1745,1749,1754,1759,1764,1769,1774,1779,1784,1789,1794,1799,1804,1809,1814,1819,1824,1829,1834,1839,1844,1849,1854,1858,1862,1866,1871,1876,1880,1884,1889,1894,1899,1904,1909,1914,1919,1924,1929,1934,1939,1944,1949,1954,1959,1964,1969,1973,1978,1983,1987,1991,1996,2001,2006,2011,2015,2020,2025,2029,2034,2039,2044,2049,2054,2059,2064,2069,2074,2079,2084,2089,2094,2099,2104,2109,2114,2119,2124,2128,2133,2137,2141,2146,2150,2155,2158,2162,2167,2172,2177,2182,2187,2192,2197,2202,2207,2212,2217,2222,2227,2232,2237,2242,2247,2252,2257,2262,2267,2272,2277,2282,2287,2292,2297,2302,2307,2312,2317,2322,2326,2331,2336,2341,2346,2351,2356,2361,2364,2369,2374,2378,2383,2388,2393,2397,2402,2407,2410,2414,2419,2424,2429,2434,2439,2444,2449,2454,2459,2464,2469,2474,2479,2483,2488,2493,2498,2503,2508,2513,2518,2523,2528,2533,2538,2543,2548,2553,2558,2563,2568,2573,2578,2581,2585,2589,2593,2598,2603,2608,2613,2618,2623,2628,2633,2638,2643,2648,2653,2658,2662,2667,2672,2677,2682,2687,2690,2694,2699,2704,2709,2714,2719,2724,2729,2734,2739,2744,2748,2753,2757,2762,2767,2772,2777,2782,2787,2792,2797,2802,2807,2812,2817,2822,2827,2832,2837,2842,2847,2852,2857,2862,2867,2872,2875,2880,2885,2890,2895,2900,2905,2910,2915,2920,2925,2930,2935,2940,2945,2950,2955,2960,2965,2970,2975,2980,2985,2990,2993,2997,3002,3007,3012,3017,3021,3026,3031,3036,3041,3046,3051,3056,3061,3066,3071,3076,3081,3084,3088,3093,3098,3103,3108,3113,3118,3123,3128,3133,3138,3142,3147,3152,3155,3159,3162,3167,3172,3177,3182,3187,3190,3195,3200,3205,3209,3214,3219,3224,3228,3232,3235,3239,3242,3247,3252,3255,3260,3265,3268,3273,3278,3282,3286,3291,3296,3301,3306,3311,3315,3320,3325,3330,3335,3340,3345,3349,3352,3357,3361,3366,3369,3373,3378,3383,3388,3393,3398,3403,3408,3413,3418,3422,3427,3431,3436,3441,3446,3451,3456,3461,3466,3471,3476,3481,3486,3490,3495,3500,3505,3510,3515,3520,3525,3530,3534,3537,3542,3547,3552,3557,3562,3567,3572,3577,3581,3586,3591,3596,3601,3606,3611,3616,3621,3626,3631,3634,3639,3644,3649,3653,3658,3663,3668,3673,3678,3683,3688,3693,3698,3703,3708,3713,3718,3723,3728,3733,3738,3742,3747,3752,3757,3762,3767,3772,3777,3781,3785,3790,3793,3798,3803,3808,3813,3816,3821,3826,3831,3835,3839,3844,3849,3854,3859,3864,3869,3874,3879,3884,3888,3893,3898,3903,3908,3913,3917,3921,3926,3931,3936,3939,3943,3948,3953,3956,3961,3966,3971,3976,3979,3984,3989,3993,3997,4001,4006,4011,4016,4021,4026,4031,4036,4041,4045,4050,4054,4059,4063,4067,4071,4076,4081,4085,4090,4095,4100,4105,4110,4115,4118,4123,4128,4133,4138,4143,4147,4152,4157,4162,4167,4172,4177,4182,4186,4191,4194,4199,4204,4207,4212,4217,4221,4226,4231,4236,4239,4242,4247,4252,4257],{"id":19,"title":18,"titles":180,"content":181,"level":182},[],"Get started with Laravel Restify in minutes. Install via Composer, run the setup command, and serve a full REST API from your Laravel application.",1,{"id":184,"title":185,"titles":186,"content":187,"level":188},"/docs/getting-started/quickstart#requirements","Requirements",[18],"Laravel Restify has a few minimum requirements you should be aware of before installing: Composer ^2.0PHP ^8.2Laravel Framework 11.x or 12.x",2,{"id":190,"title":191,"titles":192,"content":193,"level":188},"/docs/getting-started/quickstart#installation","Installation",[18],"composer require binaryk/laravel-restify",{"id":195,"title":196,"titles":197,"content":198,"level":199},"/docs/getting-started/quickstart#older-laravel-versions","Older Laravel Versions",[18,191],"For older versions of Laravel, check the appropriate branch and documentation in the GitHub repository.",3,{"id":201,"title":202,"titles":203,"content":204,"level":188},"/docs/getting-started/quickstart#setup","Setup",[18],"After installation, run the setup command to scaffold your API: php artisan restify:setup This command will: Publish the config/restify.php configuration file and action_logs migrationCreate the providers/RestifyServiceProvider and register it automaticallyCreate a new app/Restify directory for your repositoriesGenerate an abstract app/Restify/Repository.php base classScaffold a app/Restify/UserRepository for immediate useCreate the routes/ai.php file (if it doesn't exist) with commented MCP server configuration",{"id":206,"title":207,"titles":208,"content":209,"level":199},"/docs/getting-started/quickstart#run-migrations","Run Migrations",[18,202],"Complete the setup by running migrations: php artisan migrate",{"id":211,"title":212,"titles":213,"content":214,"level":188},"/docs/getting-started/quickstart#first-api-request","First API Request",[18],"With setup complete, you can immediately test your API. Laravel Restify automatically creates endpoints for your User model: # Standard pagination\nGET /api/restify/users?perPage=10&page=1\n\n# JSON:API format\nGET /api/restify/users?page[size]=10&page[number]=1",{"id":216,"title":217,"titles":218,"content":219,"level":199},"/docs/getting-started/quickstart#example-response","Example Response",[18,212],"{\n  \"data\": [\n    {\n      \"type\": \"users\",\n      \"id\": \"1\",\n      \"attributes\": {\n        \"name\": \"John Doe\",\n        \"email\": \"john@example.com\",\n        \"created_at\": \"2023-01-01T00:00:00.000000Z\"\n      }\n    }\n  ],\n  \"links\": {\n    \"first\": \"/api/restify/users?page=1\",\n    \"last\": \"/api/restify/users?page=1\",\n    \"prev\": null,\n    \"next\": null\n  },\n  \"meta\": {\n    \"current_page\": 1,\n    \"per_page\": 15,\n    \"total\": 1\n  }\n} All responses follow the JSON:API specification.",{"id":221,"title":222,"titles":223,"content":224,"level":188},"/docs/getting-started/quickstart#basic-configuration","Basic Configuration",[18],"",{"id":226,"title":227,"titles":228,"content":229,"level":199},"/docs/getting-started/quickstart#api-prefix","API Prefix",[18,222],"By default, all endpoints are prefixed with /api/restify. You can customize this in config/restify.php: 'base' => '/api/v1', // Custom prefix",{"id":231,"title":232,"titles":233,"content":234,"level":199},"/docs/getting-started/quickstart#authentication-setup","Authentication Setup",[18,222],"For production use, enable authentication by uncommenting the Sanctum middleware: 'middleware' => [\n    'api',\n    'auth:sanctum', // Uncomment this line\n    Binaryk\\LaravelRestify\\Http\\Middleware\\DispatchRestifyStartingEvent::class,\n    Binaryk\\LaravelRestify\\Http\\Middleware\\AuthorizeRestify::class,\n] Need Authentication? Check our Authentication Guide for detailed setup instructions including login endpoints and token management.",{"id":236,"title":237,"titles":238,"content":239,"level":188},"/docs/getting-started/quickstart#mcp-server-setup","MCP Server Setup",[18],"Laravel Restify can automatically generate MCP (Model Context Protocol) servers for AI agents. 1. Add the MCP server to your config/ai.php file: use Binaryk\\LaravelRestify\\MCP\\RestifyServer;\nuse Laravel\\Mcp\\Facades\\Mcp;\n\n// Web-based MCP server with authentication\nMcp::web('restify', RestifyServer::class)\n    ->middleware(['auth:sanctum'])\n    ->name('mcp.restify'); 2. Enable MCP tools in your repositories: Add the HasMcpTools trait to any repository you want to expose to AI agents: use Binaryk\\LaravelRestify\\MCP\\Concerns\\HasMcpTools;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    use HasMcpTools; // Enables MCP tools for AI agents\n    \n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required()->matchable(),\n            field('content'),\n        ];\n    }\n} This creates an MCP endpoint at /mcp/restify that AI agents can use to discover and interact with your enabled repositories automatically. Learn More: Check the MCP Server Guide for advanced configuration and usage with AI tools like Claude Desktop.",{"id":241,"title":242,"titles":243,"content":244,"level":188},"/docs/getting-started/quickstart#creating-your-first-repository","Creating Your First Repository",[18],"Let's create a Post repository to manage blog posts: # Generate repository only\nphp artisan restify:repository PostRepository\n\n# Generate everything: repository, model, migration, and policy\nphp artisan restify:repository PostRepository --all The --all flag creates: app/Restify/PostRepository.php - API repositoryapp/Models/Post.php - Eloquent modeldatabase/migrations/xxx_create_posts_table.php - Database migrationapp/Policies/PostPolicy.php - Authorization policy",{"id":246,"title":247,"titles":248,"content":249,"level":199},"/docs/getting-started/quickstart#example-repository","Example Repository",[18,242],"use Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Binaryk\\LaravelRestify\\Repositories\\Repository;\nuse Binaryk\\LaravelRestify\\Attributes\\Model;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->rules('required', 'string', 'max:255'),\n            textarea('content')->rules('required'),\n            field('author')->readonly(),\n            datetime('published_at')->nullable(),\n        ];\n    }\n} After creating the migration and running php artisan migrate, your Post API is ready at /api/restify/posts.",{"id":251,"title":252,"titles":253,"content":254,"level":188},"/docs/getting-started/quickstart#bulk-repository-generation","Bulk Repository Generation",[18],"For existing Laravel projects, generate repositories for all models at once: php artisan restify:generate:repositories This intelligent command: Discovers all models in your applicationAnalyzes database schema to map field typesShows a preview of generated repositoriesAsks for confirmation before creating filesGenerates repositories with proper field definitions",{"id":256,"title":257,"titles":258,"content":259,"level":199},"/docs/getting-started/quickstart#useful-options","Useful Options",[18,252],"# Generate for specific models only\nphp artisan restify:generate:repositories --only=User,Post\n\n# Skip preview and generate immediately  \nphp artisan restify:generate:repositories --skip-preview\n\n# Use domain-based structure\nphp artisan restify:generate:repositories --structure=domains",{"id":261,"title":262,"titles":263,"content":264,"level":199},"/docs/getting-started/quickstart#smart-field-mapping","Smart Field Mapping",[18,252],"The generator automatically maps database columns to Restify fields: Database TypeRestify Fieldanyfield() (generic base field)stringtext() (wrapper for field() )texttextarea() (wrapper for field())integernumber() (wrapper for field())booleanboolean() (wrapper for field())datetimedatetime() (wrapper for field())jsonjson() (wrapper for field()) Special Cases: Email columns → email() (wrapper for field())Password fields → password() (wrapper for field())",{"id":266,"title":267,"titles":268,"content":269,"level":188},"/docs/getting-started/quickstart#next-steps","Next Steps",[18],"Now that you have Restify running, explore these key features: Authentication - Secure your API with SanctumRepositories - Learn about fields, validation, and relationshipsSearch & Filtering - Add powerful query capabilitiesAuthorization - Control access with Laravel policiesMCP Server - Enable AI agents to interact with your API",{"id":271,"title":272,"titles":273,"content":274,"level":199},"/docs/getting-started/quickstart#generate-authorization-policies","Generate Authorization Policies",[18,267],"Create policies for fine-grained access control: php artisan restify:policy PostPolicy",{"id":276,"title":277,"titles":278,"content":279,"level":199},"/docs/getting-started/quickstart#test-your-api","Test Your API",[18,267],"Use these endpoints to test your setup: # List all users\nGET /api/restify/users\n\n# Get specific user\nGET /api/restify/users/1\n\n# Create new user (if authentication disabled)\nPOST /api/restify/users\n\n# Update user\nPATCH /api/restify/users/1\n\n# Delete user  \nDELETE /api/restify/users/1 html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":29,"title":28,"titles":281,"content":282,"level":182},[],"Set up authentication in Laravel Restify using Sanctum — register, login, logout, password reset, and email verification with one artisan command. Laravel Restify provides comprehensive authentication with Laravel Sanctum, including register, login, logout, forgot password, reset password, and email verification.",{"id":284,"title":285,"titles":286,"content":287,"level":188},"/docs/auth/authentication#quick-start","Quick start",[28],"This is everything you need to have authentication in place: If you run on Laravel 11 or higher, use this single command for complete authentication setup: php artisan restify:setup-auth This command will: Ensures that Sanctum is installed and configured as the authentication provider in the config/restify.php fileIdentifies the User model and updates the auth.user_model configuration automaticallyAppends the Route::restifyAuth(); line to the routes/api.php file to add the authentication routes That's it! Your authentication system is ready to use with register, login, logout, password reset, and email verification. Prefer manual setup? You can configure everything step-by-step as described below, starting with the Install sanctum section.",{"id":289,"title":290,"titles":291,"content":292,"level":188},"/docs/auth/authentication#install-sanctum","Install sanctum",[28],"Laravel 11 automatically ships with Sanctum, so you don't have to install it. See the documentation here. You don't need to add \\Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful::class, to your 'api' middleware group. You only have to run these 3 commands: composer require laravel/sanctum\nphp artisan vendor:publish --provider=\"Laravel\\Sanctum\\SanctumServiceProvider\"\nphp artisan migrate",{"id":294,"title":295,"titles":296,"content":297,"level":199},"/docs/auth/authentication#define-auth-model","Define auth model",[28,290],"Define your authenticatable class in the config file: 'auth' => [\n    ...\n   'user_model' => \\App\\Models\\User::class,\n] The User model should extend the Illuminate\\Foundation\\Auth\\User class or implement the Illuminate\\Contracts\\Auth\\Authenticatable interface. Make sure you have the \\Laravel\\Sanctum\\HasApiTokens trait to your User model.\nLaravel 11 will automatically add this trait to your User model. use Illuminate\\Foundation\\Auth\\User as Authenticatable;\nuse Laravel\\Sanctum\\HasApiTokens;\n\nclass User extends Authenticatable\n{\n    use HasApiTokens;",{"id":299,"title":300,"titles":301,"content":302,"level":188},"/docs/auth/authentication#define-routes","Define routes",[28],"Restify provides you a simple way to add all of your auth routes prepared. Simply add in your routes/api.php: Route::restifyAuth(); And that's it! You now have authentication routes ready to use. These are the default routes provided by restify: VerbURIActionPOST/api/registerregisterPOST/api/loginloginPOST/api/logoutlogoutPOST/api/restify/forgotPasswordforgotPasswordPOST/api/restify/resetPasswordresetPasswordPOST/api/restify/verify/{id}/{emailHash}verifyEmail The register and login routes are outside the base restify prefix because they don't have to follow the auth middleware defined in the config/restify.php config file. You can also pass an actions argument, which is an array of actions you want to register. For example: Route::restifyAuth(actions: ['login', 'register']); When using the actions argument, only the specified routes will be registered. If no actions argument is provided, Restify will register all routes by default.",{"id":304,"title":305,"titles":306,"content":307,"level":188},"/docs/auth/authentication#sanctum-middleware","Sanctum Middleware",[28],"Next, add the auth:sanctum middleware after the api middleware in your config file to protect all the restify's routes: /config/restify.php\n    'middleware' => [\n        'api',\n        'auth:sanctum',\n        ...\n    ],",{"id":309,"title":310,"titles":311,"content":312,"level":188},"/docs/auth/authentication#login","Login",[28],"Let's ensure the authentication is working correctly. Create a user in the DatabaseSeeder class: \\App\\Models\\User::factory()->create([\n   'name' => 'Test User',\n   'email' => 'test@example.com',\n   'password' => \\Illuminate\\Support\\Facades\\Hash::make('password'),\n]); Seed it: php artisan db:seed Now you can test the login with Curl or Postman: curl -X POST \"http://restify-app.test/api/login\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Content-Type: application/json\" \\\n     -d '{\n             \"email\": \"test@example.com\",\n             \"password\": \"password\"\n         }' You should see a response like this: {\n    \"id\": \"11\",\n    \"type\": \"users\",\n    \"attributes\": {\n        \"name\": \"Test User\",\n        \"email\": \"test@example.com\"\n    },\n    \"meta\": {\n        \"authorizedToShow\": true,\n        \"authorizedToStore\": false,\n        \"authorizedToUpdate\": false,\n        \"authorizedToDelete\": false,\n        \"token\": \"1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k\"\n    }\n}",{"id":314,"title":32,"titles":315,"content":316,"level":199},"/docs/auth/authentication#authorization",[28,310],"We will discuss authorization in more detail in the Authorization section. For now, let's see a simple example. After a successful login, you will receive an authentication token. You should include this token as a Bearer token in the Authorization header for your subsequent API requests using Postman, axios library, or cURL. Here's an Axios example for retrieving the user's profile with the generated token: import axios from 'axios';\n\nconst token = '1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k';\n\naxios.get('http://restify-app.test/api/restify/profile', {\n    headers: {\n        'Authorization': `Bearer ${token}`,\n        'Accept': 'application/json'\n    }\n})\n.then(response => {\n    console.log(response.data);\n})\n.catch(error => {\n    console.error(error);\n}); Here's a cURL example for retrieving the user's profile with the generated token: curl -X GET \"http://restify-app.test/api/restify/profile\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Authorization: Bearer 1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k\" Replace http://restify-app.test with your actual domain and use the authentication token you received after logging in.",{"id":318,"title":319,"titles":320,"content":321,"level":188},"/docs/auth/authentication#token-management","Token Management",[28],"Laravel Restify uses Sanctum tokens for API authentication with the following characteristics:",{"id":323,"title":324,"titles":325,"content":326,"level":199},"/docs/auth/authentication#token-expiration","Token Expiration",[28,319],"By default, tokens never expire. You can configure token expiration in your config/restify.php file: 'auth' => [\n    'token_ttl' => null, // Default: tokens never expire\n    // Set to minutes for token expiration\n    // 'token_ttl' => 60, // Tokens expire after 60 minutes (1 hour)\n]",{"id":328,"title":329,"titles":330,"content":331,"level":199},"/docs/auth/authentication#logout","Logout",[28,319],"To revoke an authentication token before it expires, use the logout endpoint: curl -X POST \"http://restify-app.test/api/logout\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Authorization: Bearer 1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k\" Successful logout returns: {\n    \"message\": \"Successfully logged out\"\n}",{"id":333,"title":334,"titles":335,"content":336,"level":199},"/docs/auth/authentication#multiple-devices","Multiple Devices",[28,319],"Laravel Sanctum automatically handles multiple device authentication: Each login creates a new tokenUsers can be logged in on multiple devices simultaneouslyEach device has its own tokenLogout only revokes the specific token used",{"id":338,"title":339,"titles":340,"content":341,"level":188},"/docs/auth/authentication#register","Register",[28],"Let's see how to register a new user in the application. You can test the registration using cURL or Postman. Use the following endpoint for registration: http://restify-app.test/api/register And send this payload: {\n    \"name\": \"John Doe\",\n    \"email\": \"demo@restify.com\",\n    \"password\": \"secret!\",\n    \"password_confirmation\": \"secret!\"\n} Note: Email and password fields are required. Now, you can send a POST request with cURL: curl -X POST \"http://restify-app.test/api/register\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Content-Type: application/json\" \\\n     -d '{\n             \"name\": \"John Doe\",\n             \"email\": \"demo@restify.com\",\n             \"password\": \"secret!\",\n             \"password_confirmation\": \"secret!\"\n         }' You should see the response like this: {\n    \"id\": \"12\",\n    \"type\": \"users\",\n    \"attributes\": {\n        \"name\": \"John Doe\",\n        \"email\": \"demo@restify.com\"\n    },\n    \"meta\": {\n        \"authorizedToShow\": true,\n        \"authorizedToStore\": false,\n        \"authorizedToUpdate\": false,\n        \"authorizedToDelete\": false,\n        \"token\": \"2|z8D2rkBLtN8GKDkjREKpwMRKTZg2ZnFqDZTSe53k\"\n    }\n}",{"id":343,"title":344,"titles":345,"content":346,"level":188},"/docs/auth/authentication#email-verification","Email Verification",[28],"Email verification is only available when your User model implements the MustVerifyEmail interface.",{"id":348,"title":349,"titles":350,"content":351,"level":199},"/docs/auth/authentication#enable-email-verification","Enable Email Verification",[28,344],"Update your User model to implement email verification: use Illuminate\\Contracts\\Auth\\MustVerifyEmail;\nuse Illuminate\\Foundation\\Auth\\User as Authenticatable;\nuse Laravel\\Sanctum\\HasApiTokens;\n\nclass User extends Authenticatable implements MustVerifyEmail\n{\n    use HasApiTokens;\n    \n    // ... rest of your User model\n} When a user registers, their email_verified_at field is set to NULL, and Restify automatically sends a verification email using the VerifyEmail notification.",{"id":353,"title":354,"titles":355,"content":356,"level":199},"/docs/auth/authentication#how-it-works","How It Works",[28,344],"When the MustVerifyEmail interface is implemented, during registration Restify will: Send verification email using Binaryk\\LaravelRestify\\Notifications\\VerifyEmailGenerate signed verification URL pointing directly to the Restify API endpointInclude parameters in the signed URL:\nid → User's IDhash → SHA1 hash of the user's email address The email contains a CTA (Call-To-Action) button that links directly to the API verification endpoint.",{"id":358,"title":359,"titles":360,"content":361,"level":199},"/docs/auth/authentication#verification-flow","Verification Flow",[28,344],"Registration: User registers → email_verified_at is NULLEmail Sent: User receives verification email with CTA linkDirect API Call: Email CTA redirects directly to /api/restify/verify/{id}/{hash}Email Verification: API validates the signed URL and sets email_verified_at to current timestampFrontend Redirect:\nSuccess: If validation succeeds, API redirects user to frontend URL from config('restify.auth.user_verify_url')Failure: If hash is invalid, API redirects to frontend with ?success=false&message=Invalid hash",{"id":363,"title":364,"titles":365,"content":366,"level":199},"/docs/auth/authentication#frontend-integration","Frontend Integration",[28,344],"With the updated flow, users click the CTA in the email and are redirected directly to the API for verification. After verification (successful or failed), they land on your frontend application. Your frontend should handle the verification result by checking URL parameters: Success case: User lands on your configured URL (no additional parameters) https://your-frontend-app.com/email/verify?id=1&hash=abc123 Failure case: User lands on your configured URL with error parameters https://your-frontend-app.com/email/verify?id=1&hash=abc123&success=false&message=Invalid hash Frontend JavaScript example: // Check for verification result\nconst urlParams = new URLSearchParams(window.location.search);\nconst success = urlParams.get('success');\nconst message = urlParams.get('message');\n\nif (success === 'false') {\n    // Handle verification failure\n    console.error('Verification failed:', message);\n    // Show error message to user\n    showErrorMessage(message || 'Email verification failed');\n} else {\n    // Handle verification success (success param is absent for successful verifications)\n    console.log('Email verified successfully');\n    // Show success message and redirect or update UI\n    showSuccessMessage('Your email has been verified successfully!');\n}",{"id":368,"title":369,"titles":370,"content":371,"level":199},"/docs/auth/authentication#curl-example","cURL Example",[28,344],"For testing purposes, you can directly call the verification endpoint: curl -X GET \"http://restify-app.test/api/restify/verify/1/abc123hash?signature=xyz&expires=123456\" \\\n     -H \"Accept: application/json\" Note: The actual verification URL includes a signed signature and expires parameter for security.",{"id":373,"title":374,"titles":375,"content":376,"level":199},"/docs/auth/authentication#success-response","Success Response",[28,344],"On successful verification, the API redirects to your configured frontend URL. For direct API calls (like cURL), you might receive a success response or redirect.",{"id":378,"title":379,"titles":380,"content":381,"level":199},"/docs/auth/authentication#configuration","Configuration",[28,344],"Configure your frontend URL in config/restify.php: 'auth' => [\n    'user_verify_url' => env('FRONTEND_APP_URL').'/email/verify?id={id}&hash={emailHash}',\n]",{"id":383,"title":384,"titles":385,"content":386,"level":188},"/docs/auth/authentication#ai-agent-authentication","AI Agent Authentication",[28],"Laravel Restify's MCP server uses the same Sanctum authentication system. AI agents authenticate using the same tokens that humans use.",{"id":388,"title":389,"titles":390,"content":391,"level":199},"/docs/auth/authentication#token-generation-for-ai-agents","Token Generation for AI Agents",[28,384],"Users can generate API tokens for AI agents through your application interface, or programmatically: // Generate a token for an AI agent\n$user = auth()->user();\n$token = $user->createToken('AI Agent Token')->plainTextToken;\n\n// Token can be used by AI agents for MCP server access",{"id":393,"title":394,"titles":395,"content":396,"level":199},"/docs/auth/authentication#mcp-server-authentication","MCP Server Authentication",[28,384],"When configuring the MCP server, tokens are passed in the Authorization header: Mcp::web('restify', RestifyServer::class)\n    ->middleware(['auth:sanctum'])  // Same authentication as REST API\n    ->name('mcp.restify');",{"id":398,"title":399,"titles":400,"content":401,"level":199},"/docs/auth/authentication#ai-agent-usage","AI Agent Usage",[28,384],"AI agents use the same Bearer token authentication: # AI agent making MCP request\ncurl -X GET \"http://restify-app.test/mcp/restify\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Authorization: Bearer 1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k\"",{"id":403,"title":404,"titles":405,"content":406,"level":199},"/docs/auth/authentication#benefits","Benefits",[28,384],"Unified Authentication: Same auth system for humans and AI agentsConsistent Permissions: Same authorization policies applyToken Management: Same logout/expiration rulesSecurity: Same security model across all interfaces Learn More: Check the MCP Server Guide for detailed AI agent configuration.",{"id":408,"title":409,"titles":410,"content":411,"level":188},"/docs/auth/authentication#forgot-password","Forgot Password",[28],"To initiate the password reset process, use the following endpoint: {{host}}/api/forgotPassword And send this payload: {\n    \"email\": \"demo@restify.com\"\n} After making a POST request to this endpoint, an email will be sent to the provided email address containing a link to reset the password. The link looks like this: 'password_reset_url' => env('FRONTEND_APP_URL').'/password/reset?token={token}&email={email}', This configuration can be found in the config/restify.php file. The FRONTEND_APP_URL should be set to the URL of your frontend app, where the user lands when they click the action button in the email. The \"token\" is a variable that will be used to reset the password later on. To view the email content during development, you can change the following configuration in your .env file: MAIL_MAILER=log This will log the email content to the laravel.log file, allowing you to see the password reset email without actually sending it. Now, you can send a POST request with cURL: curl -X POST \"http://restify-app.test/api/forgotPassword\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Content-Type: application/json\" \\\n     -d '{\n            \"email\": \"demo@restify.com\"\n         }' If the email is successfully sent, you'll receive a response similar to the following: {\n    \"message\": \"Reset password link sent to your email.\"\n} Now, the user can follow the link in the email to reset their password.",{"id":413,"title":414,"titles":415,"content":416,"level":188},"/docs/auth/authentication#reset-password","Reset Password",[28],"After the user has received the password reset email from the Forgot Password process, they can reset their password using the following endpoint: http://restify-app.test/api/resetPassword The payload should include the token and email received from the password reset email: {\n    \"token\": \"7e474bb9118e736306de27126343644a7cb0ecdaec558fdef30946d15225bc07\",\n    \"email\": \"demo@restify.com\",\n    \"password\": \"new_password\",\n    \"password_confirmation\": \"new_password\"\n} Now, you can send a POST request with cURL: curl -X POST \"http://restify-app.test/api/resetPassword\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Content-Type: application/json\" \\\n     -d '{\n             \"token\": \"0d20b6cfa48f2bbbb83bf913d5e329207149f74d7b22d59a383d321c7af7fd5e\",\n             \"email\": \"demo@restify.com\",\n             \"password\": \"new_password\",\n             \"password_confirmation\": \"new_password\"\n         }' If the password reset is successful, you should receive a response similar to the following: {\n    \"message\": \"Your password has been successfully reset.\"\n} Now the user's password has been successfully reset, and they can log in with their new password.",{"id":418,"title":419,"titles":420,"content":421,"level":188},"/docs/auth/authentication#authentication-configuration","Authentication Configuration",[28],"Laravel Restify provides several configuration options in config/restify.php under the auth key:",{"id":423,"title":424,"titles":425,"content":426,"level":199},"/docs/auth/authentication#available-options","Available Options",[28,419],"'auth' => [\n    // User model for authentication\n    'user_model' => \\App\\Models\\User::class,\n    \n    // Token expiration time in minutes (default: null - never expire)\n    'token_ttl' => null,\n    \n    // Password reset URL (for frontend)\n    'password_reset_url' => env('FRONTEND_APP_URL').'/password/reset?token={token}&email={email}',\n    \n    // Email verification URL (for frontend)\n    'user_verify_url' => env('FRONTEND_APP_URL').'/email/verify?id={id}&hash={emailHash}',\n],",{"id":428,"title":429,"titles":430,"content":431,"level":199},"/docs/auth/authentication#environment-variables","Environment Variables",[28,419],"Add these to your .env file: # Frontend application URL for auth redirects\nFRONTEND_APP_URL=https://your-frontend-app.com\n\n# Mail configuration for password reset and verification emails\nMAIL_MAILER=smtp\nMAIL_HOST=your-smtp-host\nMAIL_PORT=587\nMAIL_USERNAME=your-username\nMAIL_PASSWORD=your-password\nMAIL_ENCRYPTION=tls",{"id":433,"title":434,"titles":435,"content":436,"level":199},"/docs/auth/authentication#token-configuration","Token Configuration",[28,419],"Default TTL: null (never expire)Custom TTL: Set any value in minutesNever Expire: Set token_ttl to null // Examples\n'token_ttl' => null,  // Never expire (default)\n'token_ttl' => 30,    // 30 minutes\n'token_ttl' => 60,    // 1 hour\n'token_ttl' => 1440,  // 24 hours",{"id":438,"title":439,"titles":440,"content":441,"level":188},"/docs/auth/authentication#customizing-authentication-controllers","Customizing Authentication Controllers",[28],"You can publish the authentication controllers from the Restify package to your own application, allowing you to customize their behavior as needed. To publish the controllers, run the following command: php artisan restify:auth This command will copy the authentication controllers to the app/Http/Controllers/Restify directory in your Laravel project. The command accepts an optional --actions parameter, which allows you to specify which controllers you want to publish. If no action is passed, the command will publish all controllers and the ForgotPasswordNotification. For example, to publish only the login and register controllers, run: php artisan restify:auth --actions=login,register Now, you can make any necessary changes to these controllers to fit your specific requirements.",{"id":443,"title":444,"titles":445,"content":446,"level":199},"/docs/auth/authentication#customizing-the-register-route","Customizing the Register Route",[28,439],"In a real-world scenario, you might need to customize only the register route. To do this, you can use the restify:auth command with the --actions option to publish only the register controller: php artisan restify:auth --actions=register After running the command, the register controller will be published to your application, and you can modify it to fit your requirements. Important Note: If you want to publish other actions in the future, you'll need to manually update the routes/api.php file before running the restify:auth command again. Remove any previously published Restify routes, and keep the Route::restifyAuth(); line so that the new routes can be correctly published. For example, if you previously published the register route, your routes/api.php file might look like this: // ...\n\nRoute::restifyAuth(actions: [\"login\", \"resetPassword\", \"forgotPassword\", \"verifyEmail\"]);\n\n// ... Before running the restify:auth command again, revert the file to its original state: // ...\n\nRoute::restifyAuth();\n\n// ... Now you can run the restify:auth command with other actions, and the routes will be published correctly. html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":33,"title":32,"titles":448,"content":449,"level":182},[],"Laravel Restify unifies authorization across REST API endpoints and MCP tools using Laravel policies, covering gates, lifecycle, and access control. Laravel Restify provides a unified authorization system that protects both your REST API endpoints and MCP server tools using Laravel's built-in authorization features. This ensures that both human users and AI agents follow the same security rules when accessing your resources.",{"id":451,"title":452,"titles":453,"content":454,"level":188},"/docs/auth/authorization#unified-authorization","Unified Authorization",[32],"Restify extends Laravel's policy system to work seamlessly across different access patterns: REST API requests from web applications and mobile appsMCP tool calls from AI agents like Claude or custom AI systemsProfile endpoints for authenticated user dataBulk operations for efficient data management All authorization rules are defined once in your Laravel policies and automatically applied to both REST endpoints and MCP tools, maintaining consistent security across your entire API.",{"id":456,"title":457,"titles":458,"content":459,"level":188},"/docs/auth/authorization#request-lifecycle","Request lifecycle",[32],"Before diving into authorization details, it's important to understand the actual request lifecycle. This way, you'll know what to expect and how to debug your app at any point.",{"id":461,"title":462,"titles":463,"content":464,"level":199},"/docs/auth/authorization#booting","Booting",[32,457],"When you make a request (e.g., via Postman), it hits the Laravel application. Laravel will load every Service Provider defined in config/app.php and auto-discovered providers as well. Restify injects the RestifyApplicationServiceProvider in your config/app.php and also has an auto-discovered provider called \\Binaryk\\LaravelRestify\\LaravelRestifyServiceProvider. The LaravelRestifyServiceProvider is booted first. This pushes the RestifyInjector middleware to the end of the middleware stack.Then, the RestifyApplicationServiceProvider is booted. This defines the gate, loads repositories, and creates the auth routes macro. You have full control over this provider.The RestifyInjector is handled. It registers all the routes.On each request, if the requested route is a Restify route, Laravel will handle other middleware defined in restify.php -> middleware. This is where you should have the auth:sanctum middleware to protect your API against unauthenticated users.",{"id":466,"title":467,"titles":468,"content":469,"level":188},"/docs/auth/authorization#prerequisites","Prerequisites",[32],"Before we dive into the details of authorization, we need to make sure that you have a basic understanding of how Laravel's authorization works. If you are not familiar with it, we highly recommend reading the documentation before you move forward. You may also visit the Authentication/login section to learn how to login and use the Bearer token.",{"id":471,"title":472,"titles":473,"content":474,"level":188},"/docs/auth/authorization#view-restify","View Restify",[32],"The global viewRestify gate is the first authorization checkpoint that controls access to all Restify repositories. This gate applies to both REST API endpoints and MCP server access. This gate is only active in non-local environments.",{"id":476,"title":477,"titles":478,"content":479,"level":199},"/docs/auth/authorization#rest-api-configuration","REST API Configuration",[32,472],"The gate is defined in your RestifyApplicationServiceProvider: protected function gate()\n{\n    Gate::define('viewRestify', function ($user) {\n        return in_array($user->email, [\n            'admin@example.com',\n            'editor@example.com',\n        ]);\n    });\n}",{"id":481,"title":482,"titles":483,"content":484,"level":199},"/docs/auth/authorization#mcp-server-configuration","MCP Server Configuration",[32,472],"To apply the same viewRestify gate to your MCP server, configure it in your config/ai.php routes: use Binaryk\\LaravelRestify\\MCP\\RestifyServer;\nuse Binaryk\\LaravelRestify\\Http\\Middleware\\AuthorizeRestify;\nuse Laravel\\Mcp\\Facades\\Mcp;\n\n// MCP server with the same authorization rules as REST API\nMcp::web('restify', RestifyServer::class)\n    ->middleware(['auth:sanctum', AuthorizeRestify::class])\n    ->name('mcp.restify'); The AuthorizeRestify middleware automatically checks the viewRestify gate, ensuring that both REST endpoints and MCP tools use the same access control rules.",{"id":486,"title":487,"titles":488,"content":489,"level":199},"/docs/auth/authorization#common-gate-patterns","Common Gate Patterns",[32,472],"Recommended: Allow all authenticated users (most common approach): In most applications, you'll want to allow all authenticated users to access Restify and then control specific permissions through individual model policies: Gate::define('viewRestify', function ($user) {\n    return $user !== null; // Allow any authenticated user\n}); This approach is recommended because it provides maximum flexibility - you can then use individual model policies to control exactly what each user can do with each resource type. Role-based access: Gate::define('viewRestify', function ($user) {\n    return $user?->hasAnyRole(['admin', 'editor', 'api-user']);\n}); Permission-based access: Gate::define('viewRestify', function ($user) {\n    return $user?->hasPermissionTo('access-api');\n}); Allow unauthenticated access: Gate::define('viewRestify', function ($user = null) {\n    return true; // Use with caution - relies entirely on model policies\n}); Environment-specific access: Gate::define('viewRestify', function ($user = null) {\n    // Full access in development\n    if (app()->environment(['local', 'testing'])) {\n        return true;\n    }\n    \n    // Production: authenticated users with verified emails\n    return $user?->hasVerifiedEmail();\n});",{"id":491,"title":492,"titles":493,"content":494,"level":199},"/docs/auth/authorization#important-notes","Important Notes",[32,472],"Unified Authorization: The same gate controls both REST API and MCP server access when using the AuthorizeRestify middlewareRecommended Pattern: Most applications should return true for all authenticated users in viewRestify and handle specific permissions through model policiesFlexibility: Using return $user !== null allows you to define granular permissions at the model level rather than at the global API levelPolicy-Driven Security: Individual model policies provide fine-grained control over who can perform specific actions on specific resources Best Practice: Return true for authenticated users in viewRestify and implement detailed authorization logic in your model policies. This approach gives you maximum flexibility while maintaining security.",{"id":496,"title":497,"titles":498,"content":499,"level":188},"/docs/auth/authorization#policies","Policies",[32],"If you are not aware of what a policy is, we highly recommend reading the documentation before you move forward. You can use the Laravel command for generating a policy. It is greatly recommended to generate a policy using the Restify command because it will scaffold Restify's CRUD authorization methods for you: php artisan restify:policy UserPolicy It will automatically detect the User model (the word before Policy). However, you can also specify the model explicitly: php artisan restify:policy PostPolicy --model=Post The model is assumed to be in the `app/Models` directory. By default, Restify will deny any requests if there isn't a defined policy method associated with the request's endpoint. If you don't have a policy at all, all requests from that repository will be unauthorized. If you already have a policy, here is the Restify default scaffolded one so you can apply these methods on your own: namespace App\\Policies;\n\nuse App\\Models\\Post;\nuse App\\Models\\User;\nuse Illuminate\\Auth\\Access\\HandlesAuthorization;\n\nclass PostPolicy\n{\n    use HandlesAuthorization;\n\n    public function allowRestify(User $user = null): bool\n    {\n        //\n    }\n\n    public function show(User $user, Post $model): bool\n    {\n        //\n    }\n\n    public function store(User $user): bool\n    {\n        //\n    }\n\n    public function storeBulk(User $user): bool\n    {\n        //\n    }\n\n    public function update(User $user, Post $model): bool\n    {\n        //\n    }\n\n    public function updateBulk(User $user, Post $model): bool\n    {\n        //\n    }\n\n    public function delete(User $user, Post $model): bool\n    {\n        //\n    }\n\n    public function deleteBulk(User $user, Post $model): bool\n    {\n        //\n    }\n\n    public function restore(User $user, Post $model): bool\n    {\n        //\n    }\n\n    public function forceDelete(User $user, Post $model): bool\n    {\n        //\n    }\n} For the examples below, we will use PostRepository as our example.",{"id":501,"title":502,"titles":503,"content":504,"level":199},"/docs/auth/authorization#allow-restify","Allow restify",[32,497],"This is the gateway method that determines if a user can access any operations on this repository. This applies to both REST API requests and MCP tool calls. // PostPolicy\n/**\n * Determine whether the user can access this repository.\n * This controls access to both REST endpoints and MCP tools.\n */\npublic function allowRestify(User $user = null): bool\n{\n    // Example 1: Allow all authenticated users\n    return $user !== null;\n    \n    // Example 2: Role-based access\n    // return $user?->hasRole('editor') || $user?->hasRole('admin');\n    \n    // Example 3: Permission-based access\n    // return $user?->can('manage-posts');\n} Common patterns: // Allow specific roles\npublic function allowRestify(User $user = null): bool\n{\n    return $user?->hasAnyRole(['admin', 'editor', 'author']);\n}\n\n// Allow based on permissions\npublic function allowRestify(User $user = null): bool\n{\n    return $user?->hasPermissionTo('access-posts-api');\n}\n\n// Different access for different environments\npublic function allowRestify(User $user = null): bool\n{\n    if (app()->environment('local')) {\n        return true; // Full access in development\n    }\n    \n    return $user?->isVerified() && $user?->hasRole('content-manager');\n}",{"id":506,"title":507,"titles":508,"content":509,"level":199},"/docs/auth/authorization#allow-show","Allow show",[32,497],"Controls read access to individual records. This applies to both listing endpoints and individual resource retrieval, as well as MCP tools that query data. Routes affected: GET: /api/restify/posts (filters out unauthorized records from pagination)GET: /api/restify/posts/{id} (returns 403 if unauthorized)MCP tools: posts-show-tool, posts-index-tool /**\n * Determine whether the user can view the post.\n */\npublic function show(User $user, Post $model): bool\n{\n    // Example 1: Public posts or owned posts\n    return $model->is_public || $user->id === $model->author_id;\n    \n    // Example 2: Published posts or draft posts for owners\n    // return $model->published_at || $user->id === $model->author_id;\n    \n    // Example 3: Role-based with ownership\n    // return $user->hasRole('admin') || $user->id === $model->author_id;\n} Advanced patterns: // Status-based authorization\npublic function show(User $user, Post $model): bool\n{\n    // Admins see everything\n    if ($user->hasRole('admin')) {\n        return true;\n    }\n    \n    // Authors see their own posts\n    if ($user->id === $model->author_id) {\n        return true;\n    }\n    \n    // Regular users see published posts only\n    return $model->status === 'published';\n}\n\n// Time-based authorization\npublic function show(User $user, Post $model): bool\n{\n    // Draft posts only visible to author\n    if ($model->status === 'draft') {\n        return $user->id === $model->author_id;\n    }\n    \n    // Scheduled posts only visible after publish date\n    if ($model->status === 'scheduled') {\n        return $model->published_at \u003C= now() || $user->id === $model->author_id;\n    }\n    \n    return true; // Published posts visible to all\n}",{"id":511,"title":512,"titles":513,"content":514,"level":199},"/docs/auth/authorization#allow-store","Allow store",[32,497],"Controls the ability to create new records via both REST API and MCP tools. Routes affected: POST: /api/restify/postsMCP tools: posts-store-tool /**\n * Determine whether the user can create posts.\n */\npublic function store(User $user): bool\n{\n    // Example 1: Only verified users can create posts\n    return $user->hasVerifiedEmail();\n    \n    // Example 2: Role-based creation\n    // return $user->hasAnyRole(['author', 'editor', 'admin']);\n    \n    // Example 3: Quota-based creation\n    // return $user->posts()->where('created_at', '>=', today())->count() \u003C 5;\n} Advanced patterns: // Subscription-based authorization\npublic function store(User $user): bool\n{\n    // Free users: limited posts\n    if ($user->subscription_type === 'free') {\n        return $user->posts()->count() \u003C 10;\n    }\n    \n    // Premium users: unlimited\n    return $user->subscription_type === 'premium';\n}\n\n// Time-based restrictions\npublic function store(User $user): bool\n{\n    // New users must wait 24 hours\n    if ($user->created_at->gt(now()->subDay())) {\n        return false;\n    }\n    \n    // Rate limiting: max 3 posts per hour\n    $recentPosts = $user->posts()\n        ->where('created_at', '>=', now()->subHour())\n        ->count();\n        \n    return $recentPosts \u003C 3;\n}",{"id":516,"title":517,"titles":518,"content":519,"level":199},"/docs/auth/authorization#allow-storebulk","Allow storeBulk",[32,497],"Determine if the user can store multiple entities at once. The storeBulk method corresponds to the following route: POST: /api/restify/posts/bulk Definition: /**\n * Determine whether the user can create multiple models at once.\n *\n * @param User $user\n * @return mixed\n */\npublic function storeBulk(User $user)\n{\n    //\n}",{"id":521,"title":522,"titles":523,"content":524,"level":199},"/docs/auth/authorization#allow-update","Allow update",[32,497],"Determine if the user can update a specific model. The update method corresponds to the following routes: PUT: api/restify/posts/{id}\nPATCH: api/restify/posts/{id}\nPOST: api/restify/posts/{id} Definition: /**\n * Determine whether the user can update the model.\n *\n * @param User $user\n * @param Post $model\n * @return mixed\n */\npublic function update(User $user, Post $model)\n{\n    //\n}",{"id":526,"title":527,"titles":528,"content":529,"level":199},"/docs/auth/authorization#allow-updatebulk","Allow updateBulk",[32,497],"Determine if the user can update multiple entities at once. When you bulk update, this method will be invoked for each entity you're trying to update. If at least one will return false - none will be updated. The reason behind that is that the bulk update is a DB transaction. The updateBulk method, corresponds to the following route: POST: /api/restify/posts/bulk/update Definition: /**\n * Determine whether the user can update bulk the model.\n *\n * @param User $user\n * @param Post $model\n * @return mixed\n */\npublic function updateBulk(User $user = null, Post $model)\n{\n    return true;\n}",{"id":531,"title":532,"titles":533,"content":534,"level":199},"/docs/auth/authorization#allow-delete","Allow delete",[32,497],"The delete endpoint policy. The delete method, corresponds to the following route: DELETE: api/restify/posts/{id} Definition: /**\n * Determine whether the user can delete the model.\n *\n * @param User $user\n * @param Post $model\n * @return mixed\n */\npublic function delete(User $user, Post $model)\n{\n    //\n}",{"id":536,"title":537,"titles":538,"content":539,"level":199},"/docs/auth/authorization#allow-deletebulk","Allow deleteBulk",[32,497],"Determine if the user can delete multiple entities at once. When performing bulk deletion, this method will be invoked for each entity you're trying to delete. The deleteBulk method corresponds to the following route: DELETE: /api/restify/posts/bulk/delete Definition: /**\n * Determine whether the user can delete multiple models at once.\n *\n * @param User $user\n * @param Post $model\n * @return mixed\n */\npublic function deleteBulk(User $user, Post $model)\n{\n    //\n}",{"id":541,"title":542,"titles":543,"content":544,"level":199},"/docs/auth/authorization#allow-attach","Allow Attach",[32,497],"Here is where we're talking about pivot tables. Many to many relationships. When attaching a model to another, we should check if the user is able to do that. For example, attaching posts to a user: POST: /api/restify/users/{id}/attach/posts { \"posts\": [1, 2, 3] } Restify will guess the policy's name by the related entity. For this reason, it will be attachPost: /**\n * Determine if the post could be attached to the user.\n *\n * @param User $user\n * @param Post $model\n * @return mixed\n */\npublic function attachPost(User $user, Post $model)\n{\n    return $user->is($model->creator()->first());\n} The attachPost method will be called for each individual post.",{"id":546,"title":547,"titles":548,"content":549,"level":199},"/docs/auth/authorization#allow-detach","Allow Detach",[32,497],"Here we're talking about pivot tables. Many to many relationships. When detaching a model from another, we should check if the user is able to do that. For example, detaching posts from a user: POST: /api/restify/users/{id}/detach/posts { \"posts\": [1, 2, 3] } Restify will guess the policy's name by the related entity. For this reason, it will be detachPost: /**\n * Determine if the post could be attached to the user.\n *\n * @param User $user\n * @param Post $model\n * @return mixed\n */\npublic function detachPost(User $user, Post $model)\n{\n    return $user->is($model->creator()->first());\n} The detachPost method will be called for each individual post.",{"id":551,"title":552,"titles":553,"content":554,"level":188},"/docs/auth/authorization#mcp-authorization","MCP Authorization",[32],"When AI agents access your API through the MCP server, they use the same authentication and authorization system as regular API requests. This ensures consistent security across all access patterns.",{"id":556,"title":557,"titles":558,"content":559,"level":199},"/docs/auth/authorization#authentication-for-ai-agents","Authentication for AI Agents",[32,552],"AI agents use Laravel Sanctum Bearer tokens for authentication, but they don't authenticate directly. Instead, a human user generates a Sanctum token and provides it to the AI agent. Authentication Flow: User generates a Sanctum token for the AI agent to useToken is provided to the AI agent (manually or through your application)AI agent uses the Bearer token in all MCP tool callsSame authorization rules apply based on the token's associated user Token Generation Options: You have several approaches for generating tokens for AI agents: Option 1: Use your own Bearer token\nIf you want the AI agent to have the same permissions as yourself, you can simply give it your existing Bearer token: // Use your current authentication token\n$yourToken = auth()->user()->currentAccessToken()->plainTextToken;\n// Provide this token to your AI agent Quick Development Tip: You can easily get your Bearer token by copying it from: The initial login request response in your browser's Network tabAny XHR request headers in your browser's Developer ToolsLook for the Authorization: Bearer your-token-here header in the request headers Option 2: Generate a dedicated token for AI agents\nCreate a specific token with appropriate scopes: // Generate a token with specific abilities/scopes\n$token = auth()->user()->createToken('AI Agent Token', ['read', 'write'])->plainTextToken;\n// Provide this token to your AI agent Option 3: Create dedicated AI user accounts\nFor more control, create separate user accounts for AI agents: // Create AI agent user (one-time setup)\n$aiUser = User::create([\n    'name' => 'Claude AI Agent',\n    'email' => 'ai-agent@your-app.com',\n    'password' => Hash::make(Str::random(32)), // Random password\n]);\n\n// Assign appropriate roles/permissions\n$aiUser->assignRole('ai-agent');\n\n// Generate token for the AI user\n$token = $aiUser->createToken('MCP Server Token')->plainTextToken;\n// Provide this token to your AI agent The MCP server will authenticate requests using the provided Bearer token and apply authorization policies based on the token's associated user account.",{"id":561,"title":562,"titles":563,"content":564,"level":199},"/docs/auth/authorization#mcp-tool-authorization","MCP Tool Authorization",[32,552],"Each MCP tool respects the same policy methods as REST endpoints: // PostPolicy - these methods protect both REST and MCP access\npublic function show(User $user, Post $model): bool\n{\n    // This controls both:\n    // - GET /api/restify/posts/{id}\n    // - posts-show-tool MCP call\n    return $user->can('view', $model);\n}\n\npublic function store(User $user): bool\n{\n    // This controls both:\n    // - POST /api/restify/posts\n    // - posts-store-tool MCP call\n    return $user->hasRole('content-creator');\n}",{"id":566,"title":567,"titles":568,"content":569,"level":199},"/docs/auth/authorization#ai-agent-user-setup","AI Agent User Setup",[32,552],"Create dedicated users for AI agents with appropriate roles: // Create AI agent user\n$aiUser = User::create([\n    'name' => 'Claude AI Agent',\n    'email' => 'claude@ai.example.com',\n    'password' => Hash::make('secure-password'),\n    'email_verified_at' => now(),\n]);\n\n// Assign specific role for AI operations\n$aiUser->assignRole('ai-agent');\n\n// Or assign specific permissions\n$aiUser->givePermissionTo(['read-posts', 'create-posts', 'update-own-posts']);",{"id":571,"title":572,"titles":573,"content":574,"level":199},"/docs/auth/authorization#role-based-ai-authorization","Role-Based AI Authorization",[32,552],"Set up roles specifically for AI agents: // PostPolicy\npublic function store(User $user): bool\n{\n    return $user->hasAnyRole(['author', 'editor', 'ai-agent']);\n}\n\npublic function show(User $user, Post $model): bool\n{\n    // AI agents can read all published content\n    if ($user->hasRole('ai-agent')) {\n        return $model->status === 'published';\n    }\n    \n    // Regular user authorization\n    return $user->id === $model->author_id || $model->is_public;\n}",{"id":576,"title":577,"titles":578,"content":579,"level":199},"/docs/auth/authorization#debugging-mcp-authorization","Debugging MCP Authorization",[32,552],"Monitor AI agent authorization with logging: public function show(User $user, Post $model): bool\n{\n    $canView = $user->can('view', $model);\n    \n    // Log AI agent access attempts\n    if ($user->hasRole('ai-agent')) {\n        Log::info('AI agent access attempt', [\n            'user' => $user->email,\n            'post' => $model->id,\n            'authorized' => $canView,\n        ]);\n    }\n    \n    return $canView;\n}",{"id":581,"title":582,"titles":583,"content":584,"level":188},"/docs/auth/authorization#register-policy","Register Policy",[32],"After creating your policies, you must register them in your AuthServiceProvider. This is crucial for both REST API and MCP authorization to work properly.",{"id":586,"title":587,"titles":588,"content":589,"level":199},"/docs/auth/authorization#basic-registration","Basic Registration",[32,582],"In app/Providers/AuthServiceProvider.php: \u003C?php\n\nnamespace App\\Providers;\n\nuse App\\Models\\Post;\nuse App\\Models\\User;\nuse App\\Models\\Comment;\nuse App\\Policies\\PostPolicy;\nuse App\\Policies\\UserPolicy; \nuse App\\Policies\\CommentPolicy;\nuse Illuminate\\Foundation\\Support\\Providers\\AuthServiceProvider as ServiceProvider;\n\nclass AuthServiceProvider extends ServiceProvider\n{\n    /**\n     * The model to policy mappings for the application.\n     */\n    protected $policies = [\n        Post::class => PostPolicy::class,\n        User::class => UserPolicy::class,\n        Comment::class => CommentPolicy::class,\n    ];\n\n    public function boot(): void\n    {\n        $this->registerPolicies();\n        \n        // Additional gates or policies can be defined here\n    }\n}",{"id":591,"title":592,"titles":593,"content":594,"level":199},"/docs/auth/authorization#auto-discovery","Auto-Discovery",[32,582],"Laravel can auto-discover policies if you follow naming conventions: // These will be auto-discovered:\n// App\\Models\\Post -> App\\Policies\\PostPolicy\n// App\\Models\\User -> App\\Policies\\UserPolicy\n// App\\Models\\BlogPost -> App\\Policies\\BlogPostPolicy\n\n// If using auto-discovery, you can leave $policies empty:\nprotected $policies = [];",{"id":596,"title":597,"titles":598,"content":599,"level":199},"/docs/auth/authorization#testing-policy-registration","Testing Policy Registration",[32,582],"Verify your policies are registered correctly: // In a controller or Artisan command\npublic function testPolicies()\n{\n    $user = auth()->user();\n    $post = Post::first();\n    \n    // These should work if policies are properly registered\n    dd([\n        'can_view' => $user->can('show', $post),\n        'can_create' => $user->can('store', Post::class),\n        'can_update' => $user->can('update', $post),\n        'can_delete' => $user->can('delete', $post),\n    ]);\n}",{"id":601,"title":602,"titles":603,"content":604,"level":199},"/docs/auth/authorization#common-registration-issues","Common Registration Issues",[32,582],"Problem: Policy not being called // Wrong - model not registered\nprotected $policies = [\n    // Missing: Post::class => PostPolicy::class,\n];\n\n// Fix - register the model-policy mapping\nprotected $policies = [\n    Post::class => PostPolicy::class,\n]; Problem: Namespace issues // Wrong - incorrect namespace\nuse Policies\\PostPolicy; // Missing App\\\n\n// Fix - correct namespace\nuse App\\Policies\\PostPolicy;",{"id":606,"title":607,"titles":608,"content":609,"level":199},"/docs/auth/authorization#advanced-registration-patterns","Advanced Registration Patterns",[32,582],"public function boot(): void\n{\n    $this->registerPolicies();\n    \n    // Register multiple models to one policy\n    Gate::define('manage-content', function (User $user) {\n        return $user->hasRole('content-manager');\n    });\n    \n    // Dynamic policy registration for modular apps\n    foreach (config('modules.enabled', []) as $module) {\n        $this->registerModulePolicies($module);\n    }\n}\n\nprivate function registerModulePolicies(string $module): void\n{\n    $policiesPath = app_path(\"Modules/{$module}/Policies\");\n    \n    if (is_dir($policiesPath)) {\n        // Auto-register policies for this module\n        // Implementation depends on your modular structure\n    }\n}",{"id":611,"title":612,"titles":613,"content":614,"level":188},"/docs/auth/authorization#field-level-authorization","Field-Level Authorization",[32],"Beyond model-level authorization, you can control access to specific fields within your repositories. This is particularly useful when different users should see or modify different attributes of the same resource.",{"id":616,"title":617,"titles":618,"content":619,"level":199},"/docs/auth/authorization#repository-field-authorization","Repository Field Authorization",[32,612],"In your repository, you can conditionally include fields based on the current user's permissions: use Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\n\nclass PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        $user = $request->user();\n        \n        $fields = [\n            field('title')->required(),\n            field('content')->required(),\n            field('published_at')->rules('date'),\n        ];\n        \n        // Only admins can see/edit sensitive fields\n        if ($user->hasRole('admin')) {\n            $fields[] = field('internal_notes');\n            $fields[] = field('admin_flags');\n        }\n        \n        // Only post owners can modify status\n        if ($user->can('updateStatus', $this->model())) {\n            $fields[] = field('status')->rules('in:draft,published,archived');\n        }\n        \n        return $fields;\n    }\n}",{"id":621,"title":622,"titles":623,"content":624,"level":199},"/docs/auth/authorization#conditional-field-rules","Conditional Field Rules",[32,612],"Apply different validation rules based on user permissions: public function fields(RestifyRequest $request): array\n{\n    $user = $request->user();\n    \n    return [\n        field('title')\n            ->rules($user->hasRole('admin') ? 'required|min:5' : 'required|min:10'),\n            \n        field('content')\n            ->rules('required')\n            ->when($user->hasRole('editor'), function ($field) {\n                return $field->rules('min:500'); // Editors need longer content\n            }),\n            \n        field('featured')\n            ->rules('boolean')\n            ->when($user->hasRole('admin'), function ($field) {\n                return $field; // Only admins can set featured\n            }),\n    ];\n}",{"id":626,"title":627,"titles":628,"content":629,"level":199},"/docs/auth/authorization#related-fields-authorization","Related Fields Authorization",[32,612],"Control access to relationship fields: public static function related(): array\n{\n    $user = request()->user();\n    $relations = [];\n    \n    // Everyone can see basic relations\n    $relations['author'] = BelongsTo::make('user', UserRepository::class);\n    $relations['comments'] = HasMany::make('comments', CommentRepository::class);\n    \n    // Only admins can see sensitive relations\n    if ($user?->hasRole('admin')) {\n        $relations['audit_logs'] = HasMany::make('auditLogs', AuditLogRepository::class);\n        $relations['flags'] = MorphMany::make('flags', FlagRepository::class);\n    }\n    \n    return $relations;\n}",{"id":631,"title":632,"titles":633,"content":634,"level":199},"/docs/auth/authorization#policy-based-field-access","Policy-Based Field Access",[32,612],"Define field access in your policies: // PostPolicy\npublic function viewSensitiveFields(User $user, Post $post): bool\n{\n    return $user->hasRole('admin') || $user->id === $post->author_id;\n}\n\npublic function editMetaFields(User $user, Post $post): bool\n{\n    return $user->hasAnyRole(['admin', 'editor']);\n}\n\n// Then in your repository:\npublic function fields(RestifyRequest $request): array\n{\n    $user = $request->user();\n    $post = $this->model();\n    \n    $fields = [\n        field('title')->required(),\n        field('content')->required(),\n    ];\n    \n    if ($user->can('viewSensitiveFields', $post)) {\n        $fields[] = field('draft_notes');\n        $fields[] = field('seo_keywords');\n    }\n    \n    if ($user->can('editMetaFields', $post)) {\n        $fields[] = field('featured')->rules('boolean');\n        $fields[] = field('priority')->rules('integer|min:1|max:10');\n    }\n    \n    return $fields;\n}",{"id":636,"title":637,"titles":638,"content":639,"level":199},"/docs/auth/authorization#dynamic-field-filtering","Dynamic Field Filtering",[32,612],"Filter field values based on authorization: public function fields(RestifyRequest $request): array\n{\n    return [\n        field('title')->required(),\n        \n        field('content')->required(),\n        \n        // Mask email for non-admins\n        field('author_email')\n            ->resolveUsing(function ($value) use ($request) {\n                if ($request->user()?->hasRole('admin')) {\n                    return $value;\n                }\n                \n                return Str::mask($value, '*', 3);\n            }),\n            \n        // Show full statistics only to authorized users\n        field('view_count')\n            ->hideFromIndex(fn($request) => !$request->user()?->can('viewAnalytics'))\n            ->hideFromShow(fn($request) => !$request->user()?->can('viewAnalytics')),\n    ];\n} This field-level authorization works seamlessly with both REST API requests and MCP tool calls, ensuring consistent access control across all interfaces. For more details, see Laravel's authorization documentation. html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .svl4J, html code.shiki .svl4J{--shiki-light:#F76D47;--shiki-light-font-style:italic;--shiki-default:#F78C6C;--shiki-default-font-style:italic;--shiki-dark:#F78C6C;--shiki-dark-font-style:italic}html pre.shiki code .sFweD, html code.shiki .sFweD{--shiki-light:#E2931D;--shiki-light-font-style:italic;--shiki-default:#FFCB6B;--shiki-default-font-style:italic;--shiki-dark:#FFCB6B;--shiki-dark-font-style:italic}",{"id":37,"title":36,"titles":641,"content":642,"level":182},[],"Learn how Restify's profile endpoint lets authenticated users retrieve and update their data using UserRepository for full control over exposed fields. Laravel Restify provides a convenient profile endpoint that allows authenticated users to retrieve and update their profile information using the same repository system that powers the rest of your API.",{"id":644,"title":467,"titles":645,"content":646,"level":188},"/docs/auth/profile#prerequisites",[36],"Make sure you followed the Authentication guide first, as you need the authentication middleware configured: 'middleware' => [\n    // ...\n    'auth:sanctum',\n    // ...\n]",{"id":648,"title":649,"titles":650,"content":651,"level":188},"/docs/auth/profile#get-profile","Get profile",[36],"The profile endpoint uses your UserRepository to serialize the authenticated user's data, giving you full control over what fields are exposed and how relationships are handled. GET: /api/restify/profile Here's an example of a cURL request for retrieving the user's profile with a random token: curl -X GET \"http://your-domain.com/api/restify/profile\" \\\n     -H \"Accept: application/json\" \\\n     -H \"Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\" Replace http://your-domain.com with your actual domain and eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... with the authentication token you obtained after logging in. Here's what a basic profile response looks like: {\n    \"id\": \"7\",\n    \"type\": \"users\",\n    \"attributes\": {\n        \"name\": \"Eduard\",\n        \"email\": \"interstelar@me.com\"\n    },\n    \"meta\": {\n        \"authorizedToShow\": true,\n        \"authorizedToStore\": true,\n        \"authorizedToUpdate\": true,\n        \"authorizedToDelete\": true\n    }\n} You can add more fields in your UserRepository if you want to display them. public function fields(RestifyRequest $request): array\n{\n    return [\n        field('name')->rules('required'),\n\n        field('email')->rules('required')->storingRules('unique:users'),\n\n        field('age')\n    ];\n} Since the profile uses the UserRepository, you can include related entities using Restify's relationship system. For example, to include user roles: // UserRepository\nuse Binaryk\\LaravelRestify\\Fields\\BelongsToMany;\n\npublic static function related(): array\n{\n    return [\n        'roles' => BelongsToMany::make('roles', RoleRepository::class),\n    ];\n} Make sure your User model defines the proper Eloquent relationship: public function roles(): BelongsToMany\n{\n    return $this->belongsToMany(Role::class);\n} Now, let's get the profile by using the roles relationship: GET: /api/restify/profile?include=roles The result will look like this: {\n    \"id\": \"7\",\n    \"type\": \"users\",\n    \"attributes\": {\n        \"name\": \"Eduard\",\n        \"email\": \"interstelar@me.com\"\n    },\n    \"relationships\": {\n        \"roles\": [\n            {\n                \"id\": \"1\",\n                \"type\": \"roles\",\n                \"attributes\": {\n                    \"name\": \"owner\"\n                }\n            },\n            {\n                \"id\": \"2\", \n                \"type\": \"roles\",\n                \"attributes\": {\n                    \"name\": \"admin\"\n                }\n            }\n        ]\n    },\n    \"meta\": {\n        \"authorizedToShow\": true,\n        \"authorizedToStore\": true,\n        \"authorizedToUpdate\": true,\n        \"authorizedToDelete\": true\n    }\n}",{"id":653,"title":654,"titles":655,"content":656,"level":199},"/docs/auth/profile#repository-control","Repository Control",[36,649],"You can control whether the repository is used for profile serialization by implementing the canUseForProfile method: use Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Illuminate\\Http\\Request;\n\nclass UserRepository extends Repository\n{\n    public static function canUseForProfile(Request $request): bool\n    {\n        return true; // Always use repository for profile serialization\n    }\n    \n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('name')->rules('required'),\n            field('email')->rules('required')->storingRules('unique:users'),\n        ];\n    }\n} This method determines whether the repository should be used for profile serialization. Returning true enables full repository functionality including field control, validation, and relationships.",{"id":658,"title":659,"titles":660,"content":661,"level":188},"/docs/auth/profile#update-profile","Update profile",[36],"By default, Restify will validate and fill only the fields defined in your UserRepository when updating the user's profile. Let's use the following repository fields as an example: // UserRepository\n\npublic function fields(RestifyRequest $request)\n{\n    return [\n        field('name')->rules('required'),\n\n        field('email')->storingRules('required', 'unique:users')->messages([\n                'required' => 'This field is required.',\n            ]),\n    ];\n} If we try to call the PUT method to update the profile without data: {} We will get back a 4xx validation error: When testing via Postman (or other HTTP client), make sure you always pass the `Accept` header `application/json`. This will instruct Laravel to return JSON-formatted data. {\n    \"message\": \"The given data was invalid.\",\n    \"errors\": {\n        \"name\": [\n            \"The name field is required.\"\n        ]\n    }\n} Let's say we need to include the user name in the payload: {\n    \"name\": \"Eduard Lupacescu\"\n} Since the payload is valid now, Restify will update the user's profile (a name, in our case): {\n    \"id\": \"7\",\n    \"type\": \"users\",\n    \"attributes\": {\n        \"name\": \"Eduard Lupacescu\",\n        \"email\": \"interstelar@me.com\"\n    },\n    \"meta\": {\n        \"authorizedToShow\": true,\n        \"authorizedToStore\": true,\n        \"authorizedToUpdate\": true,\n        \"authorizedToDelete\": true\n    }\n}",{"id":663,"title":664,"titles":665,"content":666,"level":199},"/docs/auth/profile#file-uploads","File uploads",[36,659],"For file uploads (like user avatars), you must use a POST request instead of PUT or PATCH:",{"id":668,"title":669,"titles":670,"content":671,"level":188},"/docs/auth/profile#user-avatar","User avatar",[36],"To prepare your users for avatars, you can add the avatar column in your users' table: // Migration\npublic function up()\n{\n    Schema::table('users', function( Blueprint $t) {\n        $t->string('avatar')->nullable();\n    });\n} Now, you should specify in the user repository that the user has an avatar file: use Binaryk\\LaravelRestify\\Fields\\Image;\n\npublic function fields(RestifyRequest $request)\n{\n    return [\n        field('name')->rules('required'),\n\n        field('avatar')->image()->storeAs('avatar.jpg')\n    ];\n} You can use the Restify's profile update and give the avatar as an image.",{"id":673,"title":674,"titles":675,"content":676,"level":199},"/docs/auth/profile#upload-request","Upload request",[36,669],"You cannot upload a file using PUT or PATCH verbs, so you must use a POST request instead. POST: /api/restify/profile The payload should be form-data, with an image under the avatar key: {\n    \"avatar\": \"binary image in form data request\"\n} The response will be the updated profile with the new avatar URL: {\n    \"id\": \"7\",\n    \"type\": \"users\", \n    \"attributes\": {\n        \"name\": \"Eduard\",\n        \"email\": \"interstelar@me.com\",\n        \"avatar\": \"/storage/avatars/avatar.jpg\"\n    },\n    \"meta\": {\n        \"authorizedToShow\": true,\n        \"authorizedToStore\": true,\n        \"authorizedToUpdate\": true,\n        \"authorizedToDelete\": true\n    }\n} If you need to customize the path or disk for the storage file, check the image field documentation.",{"id":678,"title":679,"titles":680,"content":681,"level":188},"/docs/auth/profile#mcp-integration","MCP Integration",[36],"AI agents can access the user profile using the MCP server's profile tool. When you include the HasMcpTools trait in your UserRepository, it automatically exposes a users-profile-tool that AI agents can use to retrieve the current authenticated user's profile including relationships. use Binaryk\\LaravelRestify\\MCP\\Concerns\\HasMcpTools;\n\n#[Model(User::class)]\nclass UserRepository extends Repository\n{\n    use HasMcpTools;\n    \n    public static function canUseForProfile(Request $request): bool\n    {\n        return true;\n    }\n    \n    public static function related(): array\n    {\n        return [\n            'roles' => BelongsToMany::make('roles', RoleRepository::class),\n        ];\n    }\n} The AI agent can then use this tool to access profile information: {\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"users-profile-tool\",\n    \"arguments\": {\n      \"include\": \"roles\"\n    }\n  }\n} This provides AI agents with the same authentication and authorization controls as regular API access, ensuring secure profile data access. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"id":46,"title":45,"titles":683,"content":684,"level":182},[],"Learn how Laravel Restify repositories expose Eloquent models as REST APIs with automatic CRUD endpoints, field definitions, and full control over validation. The Repository is the core of Laravel Restify. It defines how your models are exposed through API endpoints, handling CRUD operations automatically while giving you full control over fields, validation, and authorization.",{"id":686,"title":18,"titles":687,"content":688,"level":188},"/docs/api/repositories-basic#quick-start",[45],"Create a repository with the Artisan command: php artisan restify:repository PostRepository This creates app/Restify/PostRepository.php associated with your Post model.",{"id":690,"title":691,"titles":692,"content":693,"level":188},"/docs/api/repositories-basic#basic-repository","Basic Repository",[45],"Here's a minimal repository to get you started: namespace App\\Restify;\n\nuse App\\Models\\Post;\nuse Binaryk\\LaravelRestify\\Repositories\\Repository;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Binaryk\\LaravelRestify\\Attributes\\Model;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required(),\n            field('content'),\n            field('published_at')->nullable(),\n        ];\n    }\n} That's it! You now have a complete API with these endpoints: MethodURLActionGET/api/restify/postsList all postsGET/api/restify/posts/1Get a specific postPOST/api/restify/postsCreate a new postPUT/api/restify/posts/1Update a postDELETE/api/restify/posts/1Delete a post",{"id":695,"title":696,"titles":697,"content":698,"level":188},"/docs/api/repositories-basic#model-association","Model Association",[45],"Restify needs to know which Eloquent model your repository represents. There are three ways to define this:",{"id":700,"title":701,"titles":702,"content":703,"level":199},"/docs/api/repositories-basic#_1-modern-approach-recommended","1. Modern Approach (Recommended)",[45,696],"Use PHP 8+ attributes: #[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    // Fields...\n}",{"id":705,"title":706,"titles":707,"content":708,"level":199},"/docs/api/repositories-basic#_2-static-property","2. Static Property",[45,696],"class PostRepository extends Repository\n{\n    public static string $model = Post::class;\n    \n    // Fields...\n}",{"id":710,"title":711,"titles":712,"content":713,"level":199},"/docs/api/repositories-basic#_3-auto-guessing","3. Auto-Guessing",[45,696],"If you don't specify a model, Restify will guess it from the repository name: PostRepository → App\\Models\\PostBlogPostRepository → App\\Models\\BlogPost",{"id":715,"title":69,"titles":716,"content":717,"level":188},"/docs/api/repositories-basic#fields",[45],"The fields() method defines which model attributes are exposed through your API: public function fields(RestifyRequest $request): array\n{\n    return [\n        field('title')\n            ->required()\n            ->rules('max:255'),\n            \n        field('content')\n            ->rules('required'),\n            \n        field('status')\n            ->default('draft'),\n            \n        field('published_at')\n            ->nullable(),\n    ];\n}",{"id":719,"title":720,"titles":721,"content":722,"level":199},"/docs/api/repositories-basic#field-types","Field Types",[45,69],"Common field patterns for different data: field('title')->string(),                    // Text with string validation\nfield('content')->string(),                  // Content field  \ntextarea('content'),                         // Long text (helper function)\nfield('price')->numeric(),                   // Numbers\nfield('published')->boolean(),               // True/false\nfield('published_at')->date(),               // Dates\nfield('status')->string(),                   // Status field",{"id":724,"title":725,"titles":726,"content":727,"level":199},"/docs/api/repositories-basic#field-validation","Field Validation",[45,69],"Add Laravel validation rules to fields: field('email')\n    ->required()\n    ->rules('email', 'unique:users,email'),\n\nfield('age')\n    ->rules('integer', 'min:18'),",{"id":729,"title":730,"titles":731,"content":224,"level":188},"/docs/api/repositories-basic#crud-operations","CRUD Operations",[45],{"id":733,"title":734,"titles":735,"content":736,"level":199},"/docs/api/repositories-basic#creating-posts","Creating Posts",[45,730],"Request: POST /api/restify/posts\nContent-Type: application/json\n\n{\n  \"title\": \"My First Post\",\n  \"content\": \"Hello World!\"\n} Response: {\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"posts\",\n    \"attributes\": {\n      \"title\": \"My First Post\",\n      \"content\": \"Hello World!\",\n      \"published_at\": null\n    }\n  }\n}",{"id":738,"title":739,"titles":740,"content":741,"level":199},"/docs/api/repositories-basic#reading-posts","Reading Posts",[45,730],"List all posts: GET /api/restify/posts Get specific post: GET /api/restify/posts/1",{"id":743,"title":744,"titles":745,"content":746,"level":199},"/docs/api/repositories-basic#updating-posts","Updating Posts",[45,730],"PUT /api/restify/posts/1\nContent-Type: application/json\n\n{\n  \"title\": \"Updated Title\",\n  \"published_at\": \"2024-01-15T10:00:00Z\"\n}",{"id":748,"title":749,"titles":750,"content":751,"level":199},"/docs/api/repositories-basic#deleting-posts","Deleting Posts",[45,730],"DELETE /api/restify/posts/1 Returns 204 No Content on success.",{"id":753,"title":754,"titles":755,"content":756,"level":188},"/docs/api/repositories-basic#relationships","Relationships",[45],"Define relationships in your repository to work with related models: use Binaryk\\LaravelRestify\\Fields\\BelongsTo;\nuse Binaryk\\LaravelRestify\\Fields\\HasMany;\n\nclass PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n        ];\n    }\n    \n    public static function related(): array\n    {\n        return [\n            'author' => BelongsTo::make('user', UserRepository::class),\n            'comments' => HasMany::make('comments', CommentRepository::class),\n        ];\n    }\n}",{"id":758,"title":759,"titles":760,"content":761,"level":199},"/docs/api/repositories-basic#loading-relationships","Loading Relationships",[45,754],"Include related data in your API responses: GET /api/restify/posts?related=author,comments",{"id":763,"title":764,"titles":765,"content":766,"level":188},"/docs/api/repositories-basic#search-and-filtering","Search and Filtering",[45],"Make fields searchable, sortable, or filterable: public function fields(RestifyRequest $request): array\n{\n    return [\n        field('title')\n            ->searchable()   // Can search by title\n            ->sortable(),    // Can sort by title\n            \n        field('status')\n            ->matchable(),   // Can filter by exact status\n            \n        field('content')\n            ->searchable(),  // Can search in content\n    ];\n}",{"id":768,"title":769,"titles":770,"content":771,"level":199},"/docs/api/repositories-basic#using-search-and-filters","Using Search and Filters",[45,764],"# Search for posts containing \"laravel\"\nGET /api/restify/posts?search=laravel\n\n# Filter by status\nGET /api/restify/posts?status=published\n\n# Sort by title\nGET /api/restify/posts?sort=title\n\n# Sort descending\nGET /api/restify/posts?sort=-title\n\n# Combine multiple parameters\nGET /api/restify/posts?search=laravel&status=published&sort=-created_at",{"id":773,"title":774,"titles":775,"content":776,"level":188},"/docs/api/repositories-basic#pagination","Pagination",[45],"All index requests are paginated automatically: # Get page 2 with 10 items per page\nGET /api/restify/posts?page=2&perPage=10",{"id":778,"title":32,"titles":779,"content":780,"level":188},"/docs/api/repositories-basic#authorization",[45],"Protect your repositories with Laravel policies: class PostPolicy\n{\n    public function allowRestify(User $user = null): bool\n    {\n        return true;\n    }\n    \n    public function show(User $user = null, Post $post): bool\n    {\n        return true;\n    }\n    \n    public function store(User $user): bool\n    {\n        return $user->hasRole('editor');\n    }\n    \n    public function update(User $user, Post $post): bool\n    {\n        return $user->id === $post->user_id || $user->hasRole('admin');\n    }\n    \n    public function delete(User $user, Post $post): bool\n    {\n        return $user->hasRole('admin');\n    }\n} Register your policy in AuthServiceProvider: protected $policies = [\n    Post::class => PostPolicy::class,\n];",{"id":782,"title":783,"titles":784,"content":785,"level":188},"/docs/api/repositories-basic#ai-integration-mcp","AI Integration (MCP)",[45],"Laravel Restify includes built-in support for AI agents through Model Context Protocol (MCP). To enable AI access to your repository, add the HasMcpTools trait: use Binaryk\\LaravelRestify\\Traits\\HasMcpTools;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    use HasMcpTools;\n    \n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required(),\n            field('content'),\n            field('published_at')->nullable(),\n        ];\n    }\n} This automatically creates MCP tools that AI agents can use to: List your postsRead specific postsCreate new posts (if you enable it)Update posts (if you enable it) For detailed MCP configuration, see the MCP Repositories documentation.",{"id":787,"title":788,"titles":789,"content":790,"level":188},"/docs/api/repositories-basic#whats-next","What's Next?",[45],"You now know the basics of Laravel Restify repositories! For more advanced features, check out: Advanced Repositories - Query customization, lifecycle events, custom routesFields - Advanced field types and customizationAuthorization - Advanced security and permissionsMCP Integration - Full AI agent integration html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":50,"title":49,"titles":792,"content":793,"level":182},[],"Learn how to define and register Getters in Laravel Restify — repository-attached GET endpoints with authorization, show/index scopes, and MCP integration.",{"id":795,"title":796,"titles":797,"content":798,"level":188},"/docs/api/getters#motivation","Motivation",[49],"Restify provides powerful filters and gets routes with relationships. However, sometimes you might want to get some extra data for your repositories. Let's say you have a stripe user. This is how you retrieve the stripe user information through a get request: Route::get('users/stripe-information', UserStripeController::class);\n\n// UserStripeController.php\n\npublic function __invoke(Request $request)\n{\n  ...\n} The classic approach is good, although it has a few limitations. First, you have to take care of the route middleware manually, as the testability for these endpoints should be done separately, which might be hard to maintain. At last, the endpoint is disconnected from the repository, which makes it feel out of context so has a bad readability. That way, code readability, testability, and maintainability can become hard.",{"id":800,"title":801,"titles":802,"content":803,"level":188},"/docs/api/getters#invokable-getter-format","Invokable Getter Format",[49],"The simplest way to define a getter is to use the invokable class format. Here's an example: namespace App\\Restify\\Getters;\n\nclass StripeInformationGetter\n{\n    public function __invoke()\n    {\n        return response()->json([\n            'foo' => 'bar',\n        ]);\n    }\n} Then add the getter instance to the repository getters method: ...\npublic function getters(RestifyRequest $request): array\n{\n    return [\n        new StripeInformationGetter,\n    ];\n}\n... Bellow we will see how to define getters in a more advanced way.",{"id":805,"title":806,"titles":807,"content":808,"level":188},"/docs/api/getters#getter-definition","Getter definition",[49],"Getters are very similar to actions in this sense. The big difference is that getters only allow GET requests, and should not perform any kind of DB data writing: The getter is nothing more than a class that extends the Binaryk\\LaravelRestify\\Getters\\Getter abstract class. An example of a getter class: namespace App\\Restify\\Getters;\n\nuse Binaryk\\LaravelRestify\\Getters\\Getter;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Http\\Request;use Illuminate\\Support\\Collection;\n\nclass StripeInformationGetter extends Getter\n{\n    public static $uriKey = 'stripe-information';\n    \n    public function handle(Request $request): JsonResponse\n    {\n        return response()->json([\n            'data' => $request->user()->asStripeUser()\n        ]);\n    }\n}",{"id":810,"title":811,"titles":812,"content":813,"level":199},"/docs/api/getters#register-getter","Register getter",[49,806],"Then add the getter instance to the repository getters method: public function getters(RestifyRequest $request): array\n{\n    return [\n        StripeInformationGetter::new()\n    ];\n}",{"id":815,"title":816,"titles":817,"content":818,"level":199},"/docs/api/getters#authorize-getter","Authorize getter",[49,806],"You can authorize certain getters to be active for specific users: public function getters(RestifyRequest $request): array\n{\n    return [\n        StripeInformationGetter::new()->canSee(function (Request $request) {\n            return $request->user()->can('seeStripeInfo'),\n        }),\n    ];\n}",{"id":820,"title":821,"titles":822,"content":823,"level":199},"/docs/api/getters#call-getters","Call getters",[49,806],"To call a getter, you simply access: GET: api/restify/posts/getters/stripe-information The getter query param value is the ke-bab form of the filter class name by default, or a custom $uriKey defined in the getter",{"id":825,"title":826,"titles":827,"content":828,"level":199},"/docs/api/getters#handle-getter","Handle getter",[49,806],"As soon the getter is called, the handled method will be invoked with the $request: public function handle(Request $request)\n{\n    //\n\n    return ok();\n}",{"id":830,"title":831,"titles":832,"content":833,"level":834},"/docs/api/getters#accessing-the-filtered-query","Accessing the filtered query",[49,806,826],"For index getters, you can access the filtered query builder via $request->filteredQuery(). This query builder already has all filters, search queries, and other query modifiers applied by Restify: public function handle(Request $request): JsonResponse\n{\n    // Get the filtered query builder with all applied filters, search, etc.\n    $query = $request->filteredQuery();\n\n    // You can further refine the query\n    $data = $query->where('status', 'active')->get();\n\n    return response()->json([\n        'data' => $data,\n    ]);\n} This allows you to build upon the existing query without having to manually apply filters again.",4,{"id":836,"title":837,"titles":838,"content":839,"level":188},"/docs/api/getters#getter-customizations","Getter customizations",[49],"Getters could be easily customized.",{"id":841,"title":842,"titles":843,"content":844,"level":199},"/docs/api/getters#custom-keys","Custom keys",[49,837],"Since your class names could change along the way, you can define a $uriKey property to your getters, so the frontend will use always the same getter query when applying a getter: class StripeInformationGetter extends Getter\n{\n    public static $uriKey = 'stripe-information';\n    //...\n\n};",{"id":846,"title":847,"titles":848,"content":849,"level":199},"/docs/api/getters#mcp-server-integration","MCP Server Integration",[49,837],"When using Laravel Restify with the Model Context Protocol (MCP), getters are automatically exposed as tools to AI agents. You can enhance the AI's understanding of your getters by providing descriptions and validation rules.",{"id":851,"title":852,"titles":853,"content":854,"level":834},"/docs/api/getters#getter-description","Getter Description",[49,837,847],"Provide a clear description of what your getter retrieves by setting the description property or method. This helps AI agents understand when and how to use your getter: class StripeInformationGetter extends Getter\n{\n    public string $description = 'Retrieve Stripe customer information and subscription status';\n\n    // Or override the method for dynamic descriptions\n    public function description(RestifyRequest $request): string\n    {\n        return 'Retrieve Stripe customer information and subscription status';\n    }\n\n    //...\n}",{"id":856,"title":857,"titles":858,"content":859,"level":834},"/docs/api/getters#validation-rules-for-ai-schema","Validation Rules for AI Schema",[49,837,847],"The rules() method is crucial for MCP integration. Restify automatically converts your Laravel validation rules into JSON Schema that AI agents can understand. This allows the AI to validate parameters before executing the getter: class UserAnalyticsGetter extends Getter\n{\n    public string $description = 'Get user analytics for a specific date range';\n\n    public function rules(): array\n    {\n        return [\n            'start_date' => ['required', 'date', 'before:end_date'],\n            'end_date' => ['required', 'date', 'after:start_date'],\n            'metrics' => ['array'],\n            'metrics.*' => ['string', 'in:views,clicks,conversions'],\n        ];\n    }\n\n    public function handle(Request $request, User $user): JsonResponse\n    {\n        $validated = $request->validate($this->rules());\n\n        // Getter implementation\n        return response()->json([\n            'data' => $user->analytics($validated),\n        ]);\n    }\n} The AI agent will automatically receive a JSON Schema indicating: start_date: date string (required, must be before end_date)end_date: date string (required, must be after start_date)metrics: array (optional)metrics.*: string items (must be one of: views, clicks, conversions) This schema generation works with 60+ Laravel validation rules including: email, url, uuid, integer, min, max, between, before, after, in, array, and many more.",{"id":861,"title":862,"titles":863,"content":864,"level":188},"/docs/api/getters#getters-scope","Getters scope",[49],"By default, any getter could be used on index as well as on show. However, you can choose to instruct your getter to be displayed to a specific scope.",{"id":866,"title":867,"titles":868,"content":869,"level":188},"/docs/api/getters#show-getters","Show getters",[49],"Show getters are used when you have to apply them for a single item.",{"id":871,"title":872,"titles":873,"content":874,"level":199},"/docs/api/getters#show-getter-definition","Show getter definition",[49,867],"The show getter definition differs in how it receives arguments for the handle method. Restify automatically resolves Eloquent models defined in the route id and passes them to the getter's handle method: public function handle(Request $request, User $user): JsonResponse\n{\n\n}",{"id":876,"title":877,"titles":878,"content":879,"level":199},"/docs/api/getters#show-getter-registration","Show getter registration",[49,867],"To register a show getter, we have to use the ->onlyOnShow() accessor: public function getters(RestifyRequest $request)\n{\n    return [\n        StripeInformationGetter::new()->onlyOnShow(),\n    ];\n}",{"id":881,"title":882,"titles":883,"content":884,"level":199},"/docs/api/getters#show-getter-call","Show getter call",[49,867],"The post URL should include the key of the model we want Restify to resolve: GET: api/restfiy/users/1/getters/stripe-information",{"id":886,"title":887,"titles":888,"content":889,"level":199},"/docs/api/getters#list-show-getters","List show getters",[49,867],"To get the list of available getters only for a specific model key: GET: api/api/restify/posts/1/getters",{"id":891,"title":892,"titles":893,"content":894,"level":188},"/docs/api/getters#index-getters","Index getters",[49],"Index getters are used when you have to apply them for many items.",{"id":896,"title":897,"titles":898,"content":899,"level":199},"/docs/api/getters#index-getter-definition","Index getter definition",[49,892],"The index getter definition receives only the $request in the handle method. You can access the filtered query builder using $request->filteredQuery(): public function handle(Request $request): JsonResponse\n{\n    // Get the filtered query builder with all applied filters, search, etc.\n    $query = $request->filteredQuery();\n\n    // You can further refine the query\n    $data = $query->where('status', 'active')->get();\n\n    return response()->json([\n        'data' => $data,\n    ]);\n} The filtered query builder contains all repository filters, search queries, and other query modifiers already applied. This allows you to leverage existing filters without re-implementing them.",{"id":901,"title":902,"titles":903,"content":904,"level":199},"/docs/api/getters#index-getter-registration","Index getter registration",[49,892],"To register an index getter, we have to use the ->onlyOnIndex() accessor: public function getters(RestifyRequest $request)\n{\n    return [\n        StripeInformationGetter::new()->onlyOnIndex(),\n    ];\n}",{"id":906,"title":907,"titles":908,"content":909,"level":199},"/docs/api/getters#index-getter-call","Index getter call",[49,892],"The post URL: GET: api/restfiy/posts/getters/stripe-information",{"id":911,"title":912,"titles":913,"content":914,"level":199},"/docs/api/getters#list-index-getters","List index getters",[49,892],"To get the list of available getters: GET: api/api/restify/posts/getters html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":54,"title":53,"titles":916,"content":917,"level":182},[],"Learn how Laravel Restify's Serializer and rest() helper return consistent JSON:API-style responses with fluent methods for relations and sorting.",{"id":919,"title":920,"titles":921,"content":922,"level":188},"/docs/api/serializer#introduction","Introduction",[53],"The API response format must stay consistent throughout the application. Ideally, it would be good to follow a standard as\nthe JSON:API so your frontend app could align with the API. Restify provides a convenient way to quickly return a response in a consistent format.",{"id":924,"title":925,"titles":926,"content":927,"level":188},"/docs/api/serializer#rest","rest",[53],"return rest(Company::first())\n    ->related('users')\n    ->sortDesc('id'); The rest helper accepts a list of models and returns a \\Binaryk\\LaravelRestify\\Repositories\\Serializer instance, so you can call its fluent API. The Serializer will look for the repository associated with your models. If there is a repository associated with your Company (ie CompanyRepository), Serializer will use that repository to serialize your models accordingly: {\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"companies\",\n    \"attributes\": {\n      \"name\": \"BinarCode\"\n    },\n    \"relationships\": {\n      \"users\": [\n        {\n          \"id\": \"1\",\n          \"type\": \"users\",\n          \"attributes\": {\n            \"name\": \"Eduard\",\n            \"email\": \"eduard.lupacescu@binarcode.com\"\n          },\n          \"meta\": {\n            \"authorizedToShow\": true,\n            \"authorizedToStore\": true,\n            \"authorizedToUpdate\": true,\n            \"authorizedToDelete\": true\n          },\n          \"pivots\": {\n            \"is_admin\": true\n          }\n        }\n      ]\n    },\n    \"meta\": {\n      \"authorizedToShow\": true,\n      \"authorizedToStore\": true,\n      \"authorizedToUpdate\": true,\n      \"authorizedToDelete\": true\n    }\n  }\n} In case there isn't a repository associated with your models, the response will simply be a data object with models. The rest helper accepts a model as well as a list (collection) of models, and it'll serialize the response accordingly: rest(Post::all())\n    ->related('user')\n    ->sortDesc('id')\n    ->perPage(20)",{"id":929,"title":930,"titles":931,"content":932,"level":188},"/docs/api/serializer#data","data",[53],"data(User::first(), 200) This helper simply wraps the provided data into an object with a data key: {\n  \"data\": {\n    \"id\": 1,\n    \"name\": \"User name\",\n    \"email\": \"kshlerin.hertha@example.com\"\n  }\n}",{"id":934,"title":935,"titles":936,"content":937,"level":199},"/docs/api/serializer#ok","ok",[53,930],"ok('All good!') ok helper accepts an optional message as argument, so you can return a successful response with a custom message. {\n  \"message\": \"All good!\"\n} html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .su27w, html code.shiki .su27w{--shiki-light:#916B53;--shiki-default:#916B53;--shiki-dark:#916B53}",{"id":58,"title":57,"titles":939,"content":940,"level":182},[],"Learn how Laravel Restify repositories power your REST API and AI agent integrations, with Artisan commands, field definitions, and lifecycle control. The Repository is the core of Laravel Restify, providing a unified API layer that serves both human users via REST endpoints and AI agents via MCP tools.",{"id":942,"title":943,"titles":944,"content":945,"level":188},"/docs/api/repositories#documentation-structure","Documentation Structure",[57],"New to Laravel Restify? Start with the basics: Basic Repositories - Essential concepts and getting started guide Ready for advanced features? Advanced Repositories - Query customization, lifecycle events, custom serializationMCP Integration - AI agent integration and Model Context Protocol This page contains the original comprehensive documentation. For a better learning experience, we recommend starting with the Basic Repositories guide.",{"id":947,"title":285,"titles":948,"content":949,"level":188},"/docs/api/repositories#quick-start",[57],"For convenience, Restify includes a restify:repository Artisan command. This command will create a repository\nin app/Restify directory that is associated with the App\\Models\\Post model: php artisan restify:repository PostRepository The newly created repository will be placed in the app/Restify/PostRepository.php file. By default, the generation repository command doesn't require any option. However, you can specify --app option to\ninstruct Restify to generate the migrations, policy, and model (in app/Models).",{"id":951,"title":952,"titles":953,"content":954,"level":188},"/docs/api/repositories#defining-repositories","Defining Repositories",[57],"The basic repository form looks like this using the modern attribute approach: namespace App\\Restify;\n\nuse App\\Models\\Post;\nuse Binaryk\\LaravelRestify\\Repositories\\Repository;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Binaryk\\LaravelRestify\\Attributes\\Model;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required(),\n            field('content')->string(),\n            field('published_at')->nullable(),\n        ];\n    }\n} If you don't specify the model using an attribute or the $model property, Restify will try to guess the model automatically based on the repository class name. The fields method returns the default set of attributes definitions that should be applied during API requests.",{"id":956,"title":957,"titles":958,"content":959,"level":199},"/docs/api/repositories#model-repository-discovery-conventions","Model & Repository Discovery Conventions",[57,952],"Restify will discover recursively all classes from the app\\Restify\\* directory that extend\nthe Binaryk\\LaravelRestify\\Repositories\\Repository class. For model resolution, Restify follows this priority order: #[Model] attribute (highest priority)$model static propertyAuto-guessing from repository class name (lowest priority) When auto-guessing, Restify uses the prefix of the Repository name. For example, UserPostRepository class will try to find the UserPost model.",{"id":961,"title":962,"titles":963,"content":964,"level":199},"/docs/api/repositories#actions-handled-by-the-repository","Actions handled by the Repository",[57,952],"Having this in place you're basically ready for the CRUD actions over posts. You now have available the following endpoints: VerbURIActionGET/api/restify/postsindexGET/api/restify/posts/actionsdisplay index actionsGET/api/restify/posts/gettersdisplay index gettersGET/api/restify/posts/{post}showGET/api/restify/posts/{post}/actionsdisplay individual actionsGET/api/restify/posts/{post}/gettersdisplay individual gettersPOST/api/restify/postsstorePOST/api/restify/posts/actions?action=actionNameperform index actionsGET/api/restify/posts/getters?getter=getterNameretrieve index gettersPOST/api/restify/posts/bulkstore multipleDELETE/api/restify/posts/bulk/deletedelete multiplePOST/api/restify/posts/bulk/updateupdate multiplePATCH/api/restify/posts/{post}partial updatePUT/api/restify/posts/{post}full updatePOST/api/restify/posts/{post}partial of full update including attachmentsPOST/api/restify/posts/{post}/actions?action=actionNameperform individual actionsGET/api/restify/posts/{post}/getters?getter=getterNameretrieve individual getterDELETE/api/restify/posts/{post}destroy As you can see, we provided 3 Verbs for the model update (PUT, PATCH, POST). The reason for that is\nbecause you just simply cannot send files via PATCH or PUT verbs, so we have POST as a result. The PUT or PATCH could be used\nfor full model update, and respectively partial update.",{"id":966,"title":967,"titles":968,"content":969,"level":188},"/docs/api/repositories#model-definition","Model Definition",[57],"As we already noticed, each repository basically works as a wrapper over a specific resource. The fancy\nnaming resource is nothing more than a database entity (posts, users etc.). To make the repository aware of the\nentity it should handle, we need to define the model associated with this resource. Laravel Restify provides three ways to define the model, with the following priority order:",{"id":971,"title":972,"titles":973,"content":974,"level":199},"/docs/api/repositories#_1-modern-approach-php-attributes-recommended","1. Modern Approach: PHP Attributes (Recommended)",[57,967],"The most modern and clean approach uses PHP 8+ attributes: use Binaryk\\LaravelRestify\\Attributes\\Model;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    // Clean - no static property needed\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n        ];\n    }\n} You can also use string class names: #[Model('App\\Models\\Post')]\nclass PostRepository extends Repository\n{\n    // Fields...\n}",{"id":976,"title":977,"titles":978,"content":979,"level":199},"/docs/api/repositories#_2-traditional-approach-static-property","2. Traditional Approach: Static Property",[57,967],"The classic approach using static properties (still fully supported): class PostRepository extends Repository\n{\n    public static string $model = Post::class;\n    \n    // Or with string\n    public static string $model = 'App\\\\Models\\\\Post';\n}",{"id":981,"title":982,"titles":983,"content":984,"level":199},"/docs/api/repositories#_3-auto-guessing-fallback","3. Auto-Guessing (Fallback)",[57,967],"If neither attribute nor static property is defined, Restify will automatically guess the model from the repository class name: UserRepository → tries App\\Models\\UserBlogPostRepository → tries App\\Models\\BlogPost The attribute approach takes the highest priority, followed by the static property, and finally auto-guessing as a fallback.",{"id":986,"title":987,"titles":988,"content":989,"level":188},"/docs/api/repositories#public-repository","Public repository",[57],"Sometimes, you might find yourself facing the risk of exposing public information (allowing unauthenticated users to access it). We highly recommend avoiding this kind of exposure. If you need to expose custom data, you can use the serializer to return a json:api format from any custom route/controller (still using the power of repositories). Restify allows you to define a public repository by adding the $public property on true: public static bool|array $public = true; When adding the $public flag, the repository will expose ONLY GET requests publicly. These requests are: VerbURIActionGET/api/restify/postsindexGET/api/restify/posts/gettersdisplay index gettersGET/api/restify/posts/{post}showGET/api/restify/posts/{post}/gettersdisplay individual gettersGET/api/restify/posts/getters?getter=getterNameretrieve index gettersGET/api/restify/posts/{post}/getters?getter=getterNameretrieve individual getter In order to get the public functionality you need to take a few extra steps to inform your setup that now it has public access.",{"id":991,"title":992,"titles":993,"content":994,"level":199},"/docs/api/repositories#public-gate","Public gate",[57,987],"Make sure you allow your global gate a nullable user: protected function gate(): void\n{\n    Gate::define('viewRestify', function ($user = null) {\n        if (is_null($user)) {\n           return true;\n        }\n        \n        return in_array($user->email, [...])\n    });\n}",{"id":996,"title":997,"titles":998,"content":999,"level":199},"/docs/api/repositories#public-policies","Public Policies",[57,987],"As we know, each model should be protected by a policy. The policy that corresponds to a public repository should also allow a nullable authenticated user: // ie: PostPolicy\npublic function allowRestify(User $user = null): bool\n{\n    return true;\n}\n\npublic function show(User $user = null, User $model): bool\n{\n    return true;\n} Having these configurations in place, you should be good to expose the repository publicly.",{"id":1001,"title":1002,"titles":1003,"content":1004,"level":188},"/docs/api/repositories#repository-key","Repository key",[57],"The repository URI segment is automatically generated by using the repository's name. The php method that does that is: public static function uriKey(): string\n{\n    if (property_exists(static::class, 'uriKey') && is_string(static::$uriKey)) {\n        return static::$uriKey;\n    }\n\n    $kebabWithoutRepository = Str::kebab(Str::replaceLast('Repository', '', class_basename(get_called_class())));\n\n    /**\n     * e.g. UserRepository => users\n     * e.g. LaravelEntityRepository => laravel-entities.\n     */\n    return Str::plural($kebabWithoutRepository);\n} As you can see, you can override this or define your own public static string $uriKey to the repository, so you get a custom repository uri segment. For example, if we want to call our users as members we will do as in the example below: // UserRepository\n\npublic static string $uriKey = 'members'; So the request is: GET: api/restify/members",{"id":1006,"title":69,"titles":1007,"content":1008,"level":188},"/docs/api/repositories#fields",[57],"Fields are the main component of the Repository definition. These fields represent the model's attributes that will be\nexposed through the repository's endpoints. A good practice for the API is to expose as minimum fields as you can, so\nyour API will be as private as possible. To some extent, fields are similar to the toArray method from\nthe laravel resource concept. Let's define some comprehensive fields for our Post model: use Binaryk\\LaravelRestify\\Repositories\\Repository;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\n\nclass PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')\n                ->rules('required', 'max:255')\n                ->sortable()\n                ->matchable(),\n            \n            field('slug')\n                ->rules('required', 'unique:posts,slug')\n                ->hideFromIndex(),\n            \n            field('content')\n                ->textarea()\n                ->rules('required', 'min:100')\n                ->searchable(),\n            \n            field('excerpt')\n                ->nullable()\n                ->hideFromIndex(),\n            \n            field('status')\n                ->select(['draft', 'published', 'archived'])\n                ->default('draft')\n                ->sortable()\n                ->matchable(),\n            \n            field('published_at')\n                ->nullable()\n                ->sortable(),\n            \n            field('featured')\n                ->boolean()\n                ->default(false)\n                ->matchable(),\n        ];\n    }\n} Field class has many mutations, validators and interactions that you can use. These are documented here",{"id":1010,"title":1011,"titles":1012,"content":1013,"level":188},"/docs/api/repositories#show-request","Show request",[57],"Now, your GET endpoint will expose the title and the description of the Post. The json response of\nthe api/restify/posts/1 route: {\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"posts\",\n    \"attributes\": {\n      \"title\": \"Amet ratione est quas quia ut nemo.\",\n      \"description\": null\n    },\n    \"meta\": {\n      \"authorizedToShow\": true,\n      \"authorizedToStore\": true,\n      \"authorizedToUpdate\": false,\n      \"authorizedToDelete\": false\n    }\n  }\n} Let's explain each piece of the response and see how we can impact or modify it. The id field by default is the id of the response (your table primary key). You can modify this by defining your\nown $id property into the repository:",{"id":1015,"title":1016,"titles":1017,"content":1018,"level":199},"/docs/api/repositories#id","ID",[57,1011],"public static string $id = 'uuid'; The next piece is the resource type and this is the table name. However, you can always change that by using the $type property:",{"id":1020,"title":1021,"titles":1022,"content":1023,"level":199},"/docs/api/repositories#type","Type",[57,1011],"public static string $type = 'articles'; Then, we have the attributes that are defined into the fields method.",{"id":1025,"title":1026,"titles":1027,"content":1028,"level":199},"/docs/api/repositories#meta","Meta",[57,1011],"The last piece would be the meta, where we have some authorizations over the entity. Authorizations are\ncomputed based on the policy methods. For example, the authorizedToShow represents the response of the show method\nfrom the related policy (PostPolicy in our example). You can customize the meta by creating your own resolveShowMeta method: public function resolveShowMeta($request)\n  {\n      return [\n          'is_published' => $this->model()->isPublished(),\n      ];\n  } Keep in mind that you always have access to the current model in your not static methods of the repository. In the case above, the  $this->model() represents the Post model with the id=1, because we're looking for the route: /api/restify/posts/1. As we saw before, there are many ways to partially modify the serialized response for the show request, although you\nare free to customize the entire response at once by defining: public function serializeForShow(RestifyRequest $request): array\n{\n    return [\n        //\n    ];\n}",{"id":1030,"title":1031,"titles":1032,"content":1033,"level":199},"/docs/api/repositories#custom-show","Custom show",[57,1011],"You can take full control over the show method: public function show(RestifyRequest $request, $repositoryId)\n{\n    return response($this->model());\n}",{"id":1035,"title":1036,"titles":1037,"content":1038,"level":188},"/docs/api/repositories#index-request","Index request",[57],"Since we already understood how the show method works, let's take a closer look over the endpoint that returns all\nyour entities and how it actually authorizes and serializes them. This is a standard index api/restify/posts response: {\n  \"meta\": {\n    \"current_page\": 1,\n    \"from\": 1,\n    \"last_page\": 4,\n    \"path\": \"http://restify-app.test/api/restify/posts\",\n    \"per_page\": 15,\n    \"to\": 15,\n    \"total\": 50\n  },\n  \"links\": {\n    \"first\": \"http://restify-app.test/api/restify/posts?page=1\",\n    \"last\": \"http://restify-app.test/api/restify/posts?page=4\",\n    \"prev\": null,\n    \"next\": \"http://restify-app.test/api/restify/posts?page=2\"\n  },\n  \"data\": [\n    {\n      \"id\": \"91ad2f77-e30c-4090-a79c-49417540fdaa\",\n      \"type\": \"posts\",\n      \"attributes\": {\n        \"title\": \"Nihil assumenda sit pariatur.\",\n        \"description\": null\n      },\n      \"meta\": {\n        \"authorizedToShow\": true,\n        \"authorizedToStore\": true,\n        \"authorizedToUpdate\": false,\n        \"authorizedToDelete\": false\n      }\n    },\n    ...\n    }\n  ]\n} From Restify 7+, the meta on index requests will not be loaded anymore due to performance reasons. See index item meta for more details.",{"id":1040,"title":1041,"titles":1042,"content":1043,"level":199},"/docs/api/repositories#index-main-meta","Index main meta",[57,1036],"First, we have the meta object. By default this includes pagination information, so your frontend could adapt\naccordingly. If you want to modify it, you can easily do so in the following repository: public function resolveIndexMainMeta(RestifyRequest $request, Collection $items, array $paginationMeta): ?array\n{\n    return array_merge($paginationMeta, [\n        'published_items_count' => $items->filter->isPublished()->count(),\n    ]);\n} In the resolveIndexMainMeta you get as arguments - the Restify request, a collection of items (matching the current\nrequest) and the original pagination metadata information. In the previous example we appended the property published_items_count, which counts published posts. Let's see this meta: {\n  \"meta\": {\n    \"current_page\": 1,\n    \"from\": 1,\n    \"last_page\": 4,\n    \"path\": \"http://restify-app.test/api/restify/posts\",\n    \"per_page\": 15,\n    \"to\": 15,\n    \"total\": 50,\n    \"published_items_count\": 10\n  },\n  ... You can return null if you don't need meta information.",{"id":1045,"title":1046,"titles":1047,"content":1048,"level":199},"/docs/api/repositories#index-links","Index links",[57,1036],"Next, we get an object called links. This one contains navigation links that could be used in the frontend table\ncomponent. You can customize it as well: public function resolveIndexLinks(RestifyRequest $request, Collection $items, array $links): ?array\n{\n    return $links;\n} You can return null if you don't need links information to be displayed at all. The next important property is the data. Here we have listed items matching the request query, filtered by\nthe show authorization policy. So in terms of seeing a model, you should be authorized by the model policy show method to do\nso, and if not, it will be filtered out from this response.",{"id":1050,"title":1051,"titles":1052,"content":1053,"level":199},"/docs/api/repositories#index-item-meta","Index item meta",[57,1036],"In order to optimize requests, Restify 7+ will not provide any meta information about the repositories (including nested relationships) for index requests (ie / posts). You can enable them by editing the config restify.repositories.serialize_index_meta. Or you can specifically enable them per request by adding the query param withMeta=true: GET: /api/restify/posts?withMeta=true This also applies for any related information. The individual item object format is pretty much the same as we have for the show. However, you can\nspecify a custom metadata for these items by using: public function resolveIndexMeta($request)\n{\n    return [\n        //...\n    ];\n}",{"id":1055,"title":1056,"titles":1057,"content":1058,"level":199},"/docs/api/repositories#custom-index","Custom index",[57,1036],"You're also free to define your own index method from scratch: public function index(RestifyRequest $request)\n{\n    return response(Post::all());\n}",{"id":1060,"title":1061,"titles":1062,"content":1063,"level":199},"/docs/api/repositories#index-fields","Index fields",[57,1036],"By default, attributes used to serialize the index item are the same from the fields method. Nonetheless, you can define individual fields for the index: public function fieldsForIndex(RestifyRequest $request): array\n{\n    return [\n        field('title'),\n   ];\n} Specific fields per request type could be defined for other requests. For example: fieldsForIndex, fieldsForShow, fieldsForStore\nand fieldsForUpdate.For AI agents, you can also define MCP-specific field methods like fieldsForMcpIndex, fieldsForMcpShow, etc. See the MCP Repositories documentation for details on optimizing repositories for AI agent consumption.",{"id":1065,"title":1066,"titles":1067,"content":1068,"level":188},"/docs/api/repositories#store-request","Store request",[57],"Store is a post request that is usually used to create/store entities. Let's take a closer look at the fields list for the PostRepository: public function fields(RestifyRequest $request) \n  {\n      return [\n          field('title'),\n          \n          field('description'),\n      ];\n  } Well, for the store request, Restify will use the same fields and will assign the value from the request matching the\nattribute name. Fillable Restify will fill your model's attributes (defined in the fields method) even if they are listed as $guarded. Here is the payload: {\n  \"title\": \"Beautiful day!\",\n  \"description\": \"Comming soon...\"\n} Then we have the request: POST: http://restify-app.test/api/restify/posts Restify will store the new post and will return an 201 (created) status, a Location header containing the URL to\nthe newly created entity: /api/restify/posts/1, and a data object with the newly created entity: {\n  \"data\": {\n    \"id\": \"91ad557d-5780-4e4b-bedc-c35d400d8594\",\n    \"type\": \"posts\",\n    \"attributes\": {\n      \"title\": \"Beautiful day!\",\n      \"description\": \"Comming soon...\"\n    },\n    \"meta\": {\n      \"authorizedToShow\": true,\n      \"authorizedToStore\": true,\n      \"authorizedToUpdate\": false,\n      \"authorizedToDelete\": false\n    }\n  }\n}",{"id":1070,"title":1071,"titles":1072,"content":1073,"level":199},"/docs/api/repositories#store-validation","Store Validation",[57,1066],"In a normal Laravel application, you have a store method into a controller and you have to validate fields by using this request: $request->validate([\n    'description' => 'required',\n]) To do this in Restify, you have to apply the Field's storingRules: field('description')->storingRules('required'), The rules list will be applied for the underlining field.",{"id":1075,"title":1076,"titles":1077,"content":1078,"level":199},"/docs/api/repositories#custom-store","Custom store",[57,1066],"You can always take ownership over the store method by overwriting it in the repository: public function store(RestifyRequest $request)\n{\n    //\n} The validation and authorization are done according to the store method. This method is called only if you have access and the field's validation passes.",{"id":1080,"title":1081,"titles":1082,"content":1083,"level":188},"/docs/api/repositories#update-request","Update request",[57],"Update request is similar with the store. Taking the payload: {\n  \"description\": \"Ready to be published!\"\n} And the endpoint: PUT: http://restify-app.test/api/restify/posts/1 As we saw before, we were denied by the policy from updating the operation ( \"authorizedToUpdate\":\nfalse). Now, we have to update the policy update method to return true. The Restify response contains the http 200 status and the following response: {\n  \"data\": {\n    \"id\": \"91ad557d-5780-4e4b-bedc-c35d400d8594\",\n    \"type\": \"posts\",\n    \"attributes\": {\n      \"title\": \"Beautiful day!\",\n      \"description\": \"Ready to be published!\"\n    },\n    \"meta\": {\n      \"authorizedToShow\": true,\n      \"authorizedToStore\": true,\n      \"authorizedToUpdate\": true,\n      \"authorizedToDelete\": false\n    }\n  }\n}",{"id":1085,"title":1086,"titles":1087,"content":1088,"level":199},"/docs/api/repositories#update-validation","Update validation",[57,1081],"To validate certain fields, we can use the Field's updatingRules method: field('description')->updatingRules('required'),",{"id":1090,"title":1091,"titles":1092,"content":1093,"level":199},"/docs/api/repositories#custom-update","Custom update",[57,1081],"You can override the update method entirely: public function update(RestifyRequest $request, $repositoryId)\n{\n    //\n} Keep in mind that this method is called only when the policy authorization and fields validation pass.",{"id":1095,"title":1096,"titles":1097,"content":1098,"level":188},"/docs/api/repositories#delete-request","Delete request",[57],"This request is a simple one (don't forget to allow the policy): DELETE: http://restify-app.test/api/restify/posts/1 If you're allowed to delete the resource, you will get back a 204 No content response.",{"id":1100,"title":1101,"titles":1102,"content":1103,"level":199},"/docs/api/repositories#custom-destroy","Custom destroy",[57,1096],"You can override the destory method: public function destroy(RestifyRequest $request, $repositoryId)\n{\n    //\n}",{"id":1105,"title":1106,"titles":1107,"content":1108,"level":199},"/docs/api/repositories#soft-deletion","Soft deletion",[57,1096],"Now, Restify uses the ->delete() eloquent method to delete the model. So if you're using soft deletion, it will softly delete it.",{"id":1110,"title":1111,"titles":1112,"content":1113,"level":188},"/docs/api/repositories#store-bulk-flow","Store bulk flow",[57],"The bulk store means that you can create many entries at once. For example, if you have a list of invoice entries,\nusually you have to create those in a single Database Transaction. That's why we have this way to create so many entries at\nonce: POST: /api/restify/posts/bulk With the payload: [\n  {\n    \"title\": \"Post 1\",\n    \"description\": \"Description post 1\"\n  },\n  {\n    \"title\": \"Post 2\",\n    \"description\": \"Description post 2\"\n  }\n]",{"id":1115,"title":1116,"titles":1117,"content":1118,"level":199},"/docs/api/repositories#bulk-store-field-validations","Bulk store field validations",[57,1111],"Similar with store and update methods, bulk rules has their own field rule definition: ->storeBulkRules('required', function () {}, Rule::in('posts:id')) The validation rules will be merged with the rules provided into the rules() method. The validation will be performed\nby using a native Laravel validator, so you will have exactly the same experience. The validation messages could still\nbe used as usual.",{"id":1120,"title":1121,"titles":1122,"content":1123,"level":199},"/docs/api/repositories#unauthorize-to-bulk-store","Unauthorize to bulk store",[57,1111],"In the PostPolicy you can define a method against the bulk store actions: /**\n * Determine whether the user can create multiple models at once.\n *\n * @param User $user\n * @return mixed\n */\npublic function storeBulk(User $user)\n{\n    return true;\n}",{"id":1125,"title":1126,"titles":1127,"content":1128,"level":199},"/docs/api/repositories#bulk-after-store","Bulk after store",[57,1111],"After storing an entity, the repository will call the static storedBulk method from the repository, which you can\noverride: public static function storedBulk(Collection $repositories, $request)\n{\n    //\n}",{"id":1130,"title":1131,"titles":1132,"content":1133,"level":188},"/docs/api/repositories#update-bulk-flow","Update bulk flow",[57],"Like store bulk, update bulk uses a DB transaction to perform the action. This ensures that if any entry fails, none will be updated.",{"id":1135,"title":1136,"titles":1137,"content":1138,"level":199},"/docs/api/repositories#bulk-update-field-validations","Bulk update field validations",[57,1131],"->updateBulkRules('required', Rule::in('posts:id'))",{"id":1140,"title":1141,"titles":1142,"content":1143,"level":199},"/docs/api/repositories#bulk-update-payload","Bulk update Payload",[57,1131],"The payload for a bulk update should contain an array of objects and each object should contain an id key. Based on this,\nthe Laravel Restify will find the entity: POST: /api/restify/posts/bulk/update Payload: [\n  {\n    \"id\": 1,\n    \"title\": \"First post\"\n  },\n  {\n    \"id\": 2,\n    \"title\": \"Second post\"\n  }\n]",{"id":1145,"title":1146,"titles":1147,"content":1148,"level":188},"/docs/api/repositories#bulk-delete-flow","Bulk delete flow",[57],"The payload for a bulk delete should contain an array of primary keys for the models that you want to delete: [\n  1, 10, 15\n] These models will be resolved from the database and checked for the deleteBulk policy permission. If any of the models are not allowed to be deleted, no entries will be deleted.",{"id":1150,"title":1151,"titles":1152,"content":1153,"level":188},"/docs/api/repositories#force-eager-loading","Force eager loading",[57],"Although Laravel Restify provides eager loading based on the query related property, you may want to force\neager load a relationship when using it in fields: public static $withs = ['posts']; withs is not a typo. Laravel uses the with property on models, on repositories we use $withs, it's not a typo.",{"id":1155,"title":1156,"titles":1157,"content":1158,"level":188},"/docs/api/repositories#group-by","Group by",[57],"The group by filter is useful when you want to group the results by a certain column. class PostRepository extends Repository\n{\n    public static array $groupBy = ['user_id'];\n}",{"id":1160,"title":1161,"titles":1162,"content":224,"level":188},"/docs/api/repositories#repository-collections-and-transforms","Repository Collections and Transforms",[57],{"id":1164,"title":1165,"titles":1166,"content":1167,"level":199},"/docs/api/repositories#index-collection-transform","Index Collection Transform",[57,1161],"You can transform the collection of models before they are serialized for the index response: public function indexCollection(RestifyRequest $request, Collection $items): Collection\n{\n    // Transform the entire collection\n    return $items->filter(function ($post) {\n        return $post->published_at \u003C= now();\n    });\n} This method is called after the query is executed but before authorization and serialization.",{"id":1169,"title":1170,"titles":1171,"content":224,"level":188},"/docs/api/repositories#repository-labels-and-identifiers","Repository Labels and Identifiers",[57],{"id":1173,"title":1174,"titles":1175,"content":1176,"level":199},"/docs/api/repositories#custom-repository-label","Custom Repository Label",[57,1170],"You can customize how the repository appears in API documentation and admin interfaces: class PostRepository extends Repository\n{\n    public static string $label = 'Blog Articles';\n    \n    // Or dynamically\n    public static function label(): string\n    {\n        return __('repository.posts');\n    }\n}",{"id":1178,"title":1179,"titles":1180,"content":1181,"level":199},"/docs/api/repositories#title-field","Title Field",[57,1170],"Specify which field should be used as the display title for the resource: class PostRepository extends Repository\n{\n    public static string $title = 'title'; // Default is 'id'\n    \n    public function title(): string\n    {\n        return $this->title ?: $this->id;\n    }\n    \n    public function subtitle(): ?string\n    {\n        return \"By {$this->author->name}\";\n    }\n}",{"id":1183,"title":1184,"titles":1185,"content":224,"level":188},"/docs/api/repositories#scout-integration","Scout Integration",[57],{"id":1187,"title":1188,"titles":1189,"content":1190,"level":199},"/docs/api/repositories#scout-configuration","Scout Configuration",[57,1184],"When your model uses Laravel Scout, configure search behavior: class PostRepository extends Repository\n{\n    // Number of results for global search\n    public static int $globalSearchResults = 5;\n    \n    // Number of results for Scout search\n    public static int $scoutSearchResults = 200;\n    \n    // Whether this repository should appear in global search\n    public static bool $globallySearchable = true;\n    \n    public static function usesScout(): bool\n    {\n        return true; // Detected automatically\n    }\n}",{"id":1192,"title":1193,"titles":1194,"content":224,"level":188},"/docs/api/repositories#serialization-control","Serialization Control",[57],{"id":1196,"title":1197,"titles":1198,"content":1199,"level":199},"/docs/api/repositories#custom-serialization","Custom Serialization",[57,1193],"Override serialization methods for complete control: public function serializeForIndex(RestifyRequest $request): array\n{\n    return [\n        'id' => $this->id,\n        'title' => $this->title,\n        'excerpt' => Str::limit($this->content, 100),\n        'meta' => [\n            'word_count' => str_word_count(strip_tags($this->content))\n        ]\n    ];\n}\n\npublic function serializeForShow(RestifyRequest $request): array\n{\n    $data = parent::serializeForShow($request);\n    \n    // Add custom data\n    $data['computed'] = [\n        'reading_time' => ceil(str_word_count($this->content) / 200),\n        'related_posts' => $this->getRelatedPosts(3)\n    ];\n    \n    return $data;\n}",{"id":1201,"title":1202,"titles":1203,"content":1204,"level":199},"/docs/api/repositories#restifyjs-integration","RestifyJS Integration",[57,1193],"Configure how the repository appears in RestifyJS: public function restifyjsSerialize(RestifyRequest $request): array\n{\n    return [\n        'uriKey' => static::uriKey(),\n        'related' => static::collectRelated(),\n        'sort' => static::collectFilters('sortables'),\n        'match' => static::collectFilters('matches'), \n        'searchables' => static::collectFilters('searchables'),\n        'actions' => $this->resolveActions($request)->values(),\n        'getters' => $this->resolveGetters($request)->values(),\n    ];\n}",{"id":1206,"title":1207,"titles":1208,"content":224,"level":188},"/docs/api/repositories#repository-uri-and-routing","Repository URI and Routing",[57],{"id":1210,"title":1211,"titles":1212,"content":1213,"level":199},"/docs/api/repositories#custom-uri-key","Custom URI Key",[57,1207],"Override the default URI generation: class PostRepository extends Repository\n{\n    public static string $uriKey = 'articles'; // Instead of 'posts'\n    \n    // Or dynamically\n    public static function uriKey(): string\n    {\n        return config('app.locale') === 'es' ? 'articulos' : 'articles';\n    }\n}",{"id":1215,"title":1216,"titles":1217,"content":1218,"level":199},"/docs/api/repositories#custom-routes","Custom Routes",[57,1207],"Define custom routes within the repository context: public static function routes(Router $router, array $attributes, $wrap = true)\n{\n    $router->group($attributes, function () use ($router) {\n        $router->get('trending', TrendingPostsController::class);\n        $router->post('{post}/publish', [PostController::class, 'publish']);\n        $router->get('stats', [PostStatsController::class, 'index']);\n    });\n} These routes will be available at: GET /api/restify/posts/trendingPOST /api/restify/posts/{post}/publishGET /api/restify/posts/stats",{"id":1220,"title":1221,"titles":1222,"content":224,"level":188},"/docs/api/repositories#middleware-and-security","Middleware and Security",[57],{"id":1224,"title":1225,"titles":1226,"content":1227,"level":199},"/docs/api/repositories#repository-middleware","Repository Middleware",[57,1221],"Apply middleware to all repository routes: class PostRepository extends Repository\n{\n    public static array $middleware = [\n        'throttle:60,1',\n        'verified',\n        CustomMiddleware::class,\n    ];\n    \n    public static function collectMiddlewares(RestifyRequest $request): Collection\n    {\n        $middleware = collect(static::$middleware);\n        \n        // Add conditional middleware\n        if ($request->user()?->isGuest()) {\n            $middleware->push('guest');\n        }\n        \n        return $middleware;\n    }\n}",{"id":1229,"title":1230,"titles":1231,"content":1232,"level":188},"/docs/api/repositories#repository-lifecycle-events","Repository Lifecycle Events",[57],"Laravel Restify provides several lifecycle hooks that allow you to perform actions at specific points during the repository's operations.",{"id":1234,"title":1235,"titles":1236,"content":1237,"level":199},"/docs/api/repositories#single-resource-events","Single Resource Events",[57,1230],"use Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Cache;\n\nclass PostRepository extends Repository\n{\n    // Called after a single resource is successfully stored\n    public static function stored($model, $request)\n    {\n        // Log the creation with context\n        Log::info(\"Post created: {$model->title}\", [\n            'post_id' => $model->id,\n            'user_id' => $request->user()->id,\n            'ip' => $request->ip(),\n        ]);\n        \n        // Send notifications to subscribers\n        $model->author->notify(new PostPublishedNotification($model));\n        \n        // Update caches\n        Cache::tags(['posts', 'recent'])->flush();\n        \n        // Add to search index\n        $model->searchable();\n        \n        // Update statistics\n        cache()->increment('posts_count_today');\n    }\n    \n    // Called after a single resource is successfully updated\n    public static function updated($model, $request)\n    {\n        // Log the update with changed fields\n        $dirty = $model->getDirty();\n        Log::info(\"Post updated: {$model->title}\", [\n            'post_id' => $model->id,\n            'changed_fields' => array_keys($dirty),\n            'user_id' => $request->user()->id,\n        ]);\n        \n        // Clear related caches\n        Cache::forget(\"post_{$model->id}\");\n        Cache::tags(['posts', $model->slug])->flush();\n        \n        // Re-index for search if content changed\n        if (isset($dirty['content']) || isset($dirty['title'])) {\n            $model->searchable();\n        }\n        \n        // Handle status change\n        if (isset($dirty['status']) && $model->status === 'published') {\n            event(new PostPublished($model));\n        }\n    }\n    \n    // Called after a single resource is successfully deleted\n    public static function deleted($status, $request)\n    {\n        // Log deletion with context\n        Log::info(\"Post deleted\", [\n            'status' => $status,\n            'user_id' => $request->user()->id,\n            'soft_delete' => $request->repository()->resource->trashed() ?? false,\n        ]);\n        \n        // Clean up related data only on successful deletion\n        if ($status) {\n            Cache::tags(['posts'])->flush();\n            \n            // Remove from search index\n            if (method_exists($request->repository()->resource, 'unsearchable')) {\n                $request->repository()->resource->unsearchable();\n            }\n        }\n    }\n}",{"id":1239,"title":1240,"titles":1241,"content":1242,"level":199},"/docs/api/repositories#bulk-operation-events","Bulk Operation Events",[57,1230],"class PostRepository extends Repository\n{\n    // Called after bulk store operation completes\n    public static function storedBulk(Collection $models, $request)\n    {\n        Log::info(\"Bulk created {$models->count()} posts\");\n        \n        // Bulk index for search\n        $models->searchable();\n        \n        // Send bulk notifications\n        NotificationService::notifyBulkCreation($models);\n    }\n    \n    // Called after bulk update operation completes  \n    public static function updatedBulk(Collection $models, $request)\n    {\n        Log::info(\"Bulk updated {$models->count()} posts\");\n        \n        // Clear caches\n        cache()->tags(['posts'])->flush();\n    }\n    \n    // Called after bulk save operation (both store and update bulk)\n    public static function savedBulk(Collection $models, $request)\n    {\n        // Common logic for all bulk save operations\n        SearchIndexService::updateBatch($models);\n    }\n    \n    // Called after bulk delete operation completes\n    public static function deletedBulk(Collection $models, $request)\n    {\n        Log::info(\"Bulk deleted {$models->count()} posts\");\n    }\n}",{"id":1244,"title":1245,"titles":1246,"content":1247,"level":199},"/docs/api/repositories#authorization-methods","Authorization Methods",[57,1230],"Override authorization logic for fine-grained control: class PostRepository extends Repository\n{\n    // Control if user can view the resource\n    public function allowToShow($request): self\n    {\n        if (!$this->model()->isPublished() && !$request->user()->isAdmin()) {\n            throw new AuthorizationException('Cannot view unpublished post');\n        }\n        \n        return $this;\n    }\n    \n    // Control if user can create resources\n    public function allowToStore(RestifyRequest $request, $payload = null): self\n    {\n        if ($request->user()->posts()->today()->count() >= 10) {\n            throw new AuthorizationException('Daily post limit reached');\n        }\n        \n        return $this;\n    }\n    \n    // Control if user can update this resource\n    public function allowToUpdate(RestifyRequest $request, $payload = null): self\n    {\n        if ($this->model()->isPublished() && !$request->user()->isEditor()) {\n            throw new AuthorizationException('Cannot edit published posts');\n        }\n        \n        return $this;\n    }\n    \n    // Control if user can delete this resource\n    public function allowToDestroy(RestifyRequest $request): self\n    {\n        if ($this->model()->comments()->exists()) {\n            throw new AuthorizationException('Cannot delete post with comments');\n        }\n        \n        return $this;\n    }\n    \n    // Control bulk operations\n    public function allowToBulkStore(RestifyRequest $request, $payload = null, $row = null): self\n    {\n        if (count($payload) > 100) {\n            throw new AuthorizationException('Cannot create more than 100 posts at once');\n        }\n        \n        return $this;\n    }\n    \n    public function allowToUpdateBulk(RestifyRequest $request, $payload = null): self\n    {\n        // Custom bulk update authorization\n        return $this;\n    }\n    \n    public function allowToDestroyBulk(RestifyRequest $request, $payload = null): self\n    {\n        // Custom bulk delete authorization  \n        return $this;\n    }\n}",{"id":1249,"title":1250,"titles":1251,"content":1252,"level":199},"/docs/api/repositories#relationship-authorization","Relationship Authorization",[57,1230],"class PostRepository extends Repository\n{\n    public function allowToAttach(RestifyRequest $request, Collection $attachers): self\n    {\n        // Validate attaching related models\n        $methodGuesser = 'attach'.Str::studly($request->relatedRepository);\n        \n        $attachers->each(function ($model) use ($request, $methodGuesser) {\n            $this->authorizeToAttach($request, $methodGuesser, $model);\n        });\n        \n        return $this;\n    }\n    \n    public function allowToSync(RestifyRequest $request, Collection $attachers): self\n    {\n        // Validate syncing relationships\n        return $this;\n    }\n    \n    public function allowToDetach(RestifyRequest $request, Collection $attachers): self\n    {\n        // Validate detaching related models\n        return $this;\n    }\n}",{"id":1254,"title":1255,"titles":1256,"content":1257,"level":199},"/docs/api/repositories#event-usage-examples","Event Usage Examples",[57,1230],"These lifecycle methods are perfect for: Logging and Auditing: Track all changes to your resourcesCache Management: Clear or update caches when data changesSearch Indexing: Update search indexes after modificationsNotifications: Send emails, push notifications, or webhooksData Validation: Perform complex business rule validationExternal API Integration: Sync changes with third-party servicesFile Cleanup: Remove associated files when records are deleted html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .svl4J, html code.shiki .svl4J{--shiki-light:#F76D47;--shiki-light-font-style:italic;--shiki-default:#F78C6C;--shiki-default-font-style:italic;--shiki-dark:#F78C6C;--shiki-dark-font-style:italic}html pre.shiki code .sFweD, html code.shiki .sFweD{--shiki-light:#E2931D;--shiki-light-font-style:italic;--shiki-default:#FFCB6B;--shiki-default-font-style:italic;--shiki-dark:#FFCB6B;--shiki-dark-font-style:italic}",{"id":62,"title":61,"titles":1259,"content":1260,"level":182},[],"Master advanced Laravel Restify repository features: query customization, lifecycle hooks, public repositories, custom routes, and performance tuning. This guide covers advanced repository features for experienced Laravel Restify users. If you're new to Restify, start with the Basic Repositories guide.",{"id":1262,"title":1263,"titles":1264,"content":1265,"level":188},"/docs/api/repositories-advanced#overview","Overview",[61],"Advanced repository features include: Query Customization - Control how data is fetched from the databaseCustom Field Methods - Different fields for different operationsPublic Repositories - Allow unauthenticated accessRepository Lifecycle - Hook into CRUD operationsCustom Routes - Add your own endpointsPerformance Optimization - Bulk operations, caching, eager loading",{"id":1267,"title":1268,"titles":1269,"content":1270,"level":188},"/docs/api/repositories-advanced#query-customization","Query Customization",[61],"Restify provides several methods to customize how data is queried from your database. These methods are called in a specific order, allowing you to build complex query logic.",{"id":1272,"title":1273,"titles":1274,"content":1275,"level":199},"/docs/api/repositories-advanced#main-query","Main query",[61,1268],"The mainQuery method is called for ALL repository operations and serves as the base query that other query methods build upon. This is the foundational query method that's applied to show, index, global search, and all other requests: // PostRepository\n\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\npublic static function mainQuery(RestifyRequest $request, Builder | Relation $query)\n{\n    return $query->where('company_id', $request->user()->company_id);\n} This method is ideal for: Global scoping (e.g., multi-tenancy isolation)Common filtering logic that applies to all operationsSecurity constraints that should never be bypassedGlobal eager loading for frequently used relationships",{"id":1277,"title":1278,"titles":1279,"content":1280,"level":199},"/docs/api/repositories-advanced#index-query","Index query",[61,1268],"The indexQuery method is specifically called for listing operations (GET /api/restify/posts) and global search requests. It builds on top of the mainQuery: // PostRepository\n\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\npublic static function indexQuery(RestifyRequest $request, Builder | Relation $query)\n{\n    return $query->where('status', 'published')\n                 ->with('author:id,name')\n                 ->orderBy('published_at', 'desc');\n} This method is perfect for: Index-specific filtering (e.g., only show published items)Default sorting for listingsPerformance optimizations for list viewsLightweight eager loading for index displays",{"id":1282,"title":1283,"titles":1284,"content":1285,"level":199},"/docs/api/repositories-advanced#show-query","Show query",[61,1268],"The showQuery method is applied specifically for individual resource requests (GET /api/restify/posts/1). It allows you to customize queries when fetching a single resource: // PostRepository\n\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\npublic static function showQuery(RestifyRequest $request, Builder | Relation $query)\n{\n    return $query->with(['author', 'categories', 'tags', 'comments.user']);\n} This method is useful for: Detailed eager loading for full resource displayShow-specific constraints or permissionsPerformance optimizations for single resource fetching",{"id":1287,"title":1288,"titles":1289,"content":1290,"level":199},"/docs/api/repositories-advanced#scout-query","Scout query",[61,1268],"When using Laravel Scout for full-text search, you can customize the Scout query builder: // PostRepository\n\npublic static function scoutQuery(RestifyRequest $request, $scoutBuilder)\n{\n    return $scoutBuilder->where('status', 'published')\n                       ->where('tenant_id', $request->user()->tenant_id);\n}",{"id":1292,"title":1293,"titles":1294,"content":1295,"level":199},"/docs/api/repositories-advanced#query-method-hierarchy","Query Method Hierarchy",[61,1268],"The query methods are applied in this specific order: Base Query: query() - Creates the initial query builder from the modelMain Query: mainQuery() - Applied to ALL operations for global constraintsSpecific Query: indexQuery() or showQuery() - Applied based on the operation typeSearch/Filters: Applied by Restify's search service based on request parametersScout Query: scoutQuery() - Only applied when using Laravel Scout search",{"id":1297,"title":1298,"titles":1299,"content":1300,"level":199},"/docs/api/repositories-advanced#complete-example-multi-tenant-blog-repository","Complete Example: Multi-tenant Blog Repository",[61,1268],"class PostRepository extends Repository\n{\n    // Step 2: Applied to ALL operations - ensures tenant isolation\n    public static function mainQuery(RestifyRequest $request, $query)\n    {\n        return $query->where('tenant_id', $request->user()->tenant_id)\n                     ->whereNull('deleted_at'); // Global soft delete check\n    }\n    \n    // Step 3a: Only for listing - show published posts with minimal data\n    public static function indexQuery(RestifyRequest $request, $query)\n    {\n        return $query->where('status', 'published')\n                     ->with('author:id,name,avatar')\n                     ->orderBy('published_at', 'desc');\n    }\n    \n    // Step 3b: Only for individual posts - load complete relationships\n    public static function showQuery(RestifyRequest $request, $query)\n    {\n        return $query->with([\n            'author',\n            'categories',\n            'tags',\n            'comments' => function ($query) {\n                $query->where('approved', true)->with('user:id,name');\n            }\n        ]);\n    }\n    \n    // Step 5: Scout search within tenant boundaries\n    public static function scoutQuery(RestifyRequest $request, $scoutBuilder)\n    {\n        return $scoutBuilder->where('tenant_id', $request->user()->tenant_id)\n                           ->where('status', 'published');\n    }\n}",{"id":1302,"title":1303,"titles":1304,"content":1305,"level":188},"/docs/api/repositories-advanced#repository-prefix","Repository prefix",[61],"The default prefix of all Restify routes (except login and register) lives under the restify->base config: ...\n'base' => '/api/restify',\n... Thus, Restify generates the URI for the repository in the following way: config('restify.base') . '/' . UserRepository::uriKey() . '/' For example, let's assume we have the restify.base equal with: api/restify. The default URI generated for the\nUserRepository is: GET: /api/restify/users However, you can prefix the repository with your own: public static $prefix = 'api/v1'; Now, the generated URI will look like this: GET: /api/v1/users For the rest of the repositories the prefix will stay as it is, the default one. Keep in mind that this custom prefix\nwill be used for all the endpoints related to the user repository.",{"id":1307,"title":1308,"titles":1309,"content":1310,"level":188},"/docs/api/repositories-advanced#repository-middleware","Repository middleware",[61],"Each repository has the middlewares from the config restify.middleware out of the box for the CRUD methods. However,\nyou're free to add your own middlewares for a specific repository. public static $middleware = [\n        NeedsCompanyMiddleware::class,\n    ]; This NeedsCompanyMiddleware is a custom middleware, and it will be applied over all CRUD routes for this repository. If you need the current request, you can override the collectMiddlewares method and use the current request: public static function collectMiddlewares(RestifyRequest $request): ?Collection\n{\n    if ($request->isShowRequest())         \n    {\n        return collect([ \n            NeedsCompanyMiddleware::class,\n        ]);\n    }\n\n    if ($request->isIndexRequest())         \n    {\n        return collect([ \n            SampleIndexRequest::class,\n        ]);\n    }\n\n    return null;\n}",{"id":1312,"title":1313,"titles":1314,"content":1315,"level":188},"/docs/api/repositories-advanced#repository-registration","Repository registration",[61],"Laravel Restify registers all repositories automatically in the App namespace. However, you can register your own repositories from any service provider using the InteractsWithRestifyRepositories trait. Here's an example: \u003C?php\n\nnamespace MyPackage\\Cart;\n\nuse Binaryk\\LaravelRestify\\Traits\\InteractsWithRestifyRepositories;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass MyPackageCart extends ServiceProvider\n{\n    use InteractsWithRestifyRepositories;\n\n    public function register(): void\n    {\n        $this->loadRestifyFrom(__DIR__.'/Restify', __NAMESPACE__.'\\\\Restify\\\\');\n        \n        // The rest of your package's registration code goes here.\n    }\n} If you want to load Restify from your own service provider, you must use the InteractsWithRestifyRepositories trait in the service provider class. The loadRestifyFrom method takes the path to the directory containing the repositories and the namespace under which the repositories will be registered.",{"id":1317,"title":1318,"titles":1319,"content":1320,"level":188},"/docs/api/repositories-advanced#dependency-injection","Dependency injection",[61],"The Laravel service container is used to resolve all the Laravel Restify\nrepositories. As a result, you are able to type-hint any dependencies your Repository may need in its constructor. The\ndeclared dependencies will automatically be resolved and injected into the repository's instance: Important: Don't forget to call the parent constructor. use App\\Services\\PostService;\nuse App\\Restify\\Repository;\n\nclass PostRepository extends Repository\n{\n   private PostService $postService; \n\n   public function __construct(PostService $service)\n   {\n       parent::__construct();\n\n       $this->postService = $service;\n   }\n}",{"id":1322,"title":1323,"titles":1324,"content":1325,"level":199},"/docs/api/repositories-advanced#custom-crud","Custom CRUD",[61,1318],"Restify injects all CRUD's operations for you. However, sometimes you may want to intercept or override\nthe entire logic of a specific action. Let's say your save method needs to perform additional operations beyond the default action. In\nthis case you can easily override each action (defined here) in the repository:",{"id":1327,"title":1328,"titles":1329,"content":1330,"level":199},"/docs/api/repositories-advanced#index","index",[61,1318],"public function index(RestifyRequest $request)\n    {\n        // Silence is golden\n    }",{"id":1332,"title":1333,"titles":1334,"content":1335,"level":199},"/docs/api/repositories-advanced#show","show",[61,1318],"public function show(RestifyRequest $request, $repositoryId)\n    {\n        // Silence is golden\n    }",{"id":1337,"title":1338,"titles":1339,"content":1340,"level":199},"/docs/api/repositories-advanced#store","store",[61,1318],"public function store(RestifyRequest $request)\n    {\n        // Silence is golden\n    }",{"id":1342,"title":1343,"titles":1344,"content":1345,"level":199},"/docs/api/repositories-advanced#store-bulk","store bulk",[61,1318],"public function storeBulk(RepositoryStoreBulkRequest $request)\n    {\n        // Silence is golden\n    }",{"id":1347,"title":1348,"titles":1349,"content":1350,"level":199},"/docs/api/repositories-advanced#update","update",[61,1318],"public function update(RestifyRequest $request, $repositoryId)\n    {\n        // Silence is golden\n    }",{"id":1352,"title":1353,"titles":1354,"content":1355,"level":199},"/docs/api/repositories-advanced#update-bulk","update bulk",[61,1318],"// $row is the payload row to be updated\n    public function updateBulk(RestifyRequest $request, $repositoryId, int $row)\n    {\n        // Silence is golden\n    }",{"id":1357,"title":1358,"titles":1359,"content":1360,"level":199},"/docs/api/repositories-advanced#destroy","destroy",[61,1318],"public function destroy(RestifyRequest $request, $repositoryId)\n    {\n        // Silence is golden\n    }",{"id":1362,"title":1363,"titles":1364,"content":1365,"level":188},"/docs/api/repositories-advanced#custom-routes","Custom routes",[61],"Laravel Restify has its own \"CRUD\" routes, although you're able to define your custom routes right from your Repository\nclass: /**\n * Defining custom routes\n * \n * The default prefix of this route is the uriKey (e.g. 'api/restify/posts'),\n * \n * The default namespace is AppNamespace/Http/Controllers\n * \n * The default middlewares are the same from config('restify.middleware')\n *\n * All options could be overrided by passing an $attributes argument and set $wrap to false\n *\n * @param  \\Illuminate\\Routing\\Router  $router\n * @param $attributes\n */\npublic static function routes(\\Illuminate\\Routing\\Router $router, $attributes = [], $wrap = true)\n{\n    $router->get('last-posts', function () {\n        return static::makeModel()->latest()->first();\n    });\n\n    $router->post('make-primary/{post}', [static::class, 'makePrimary']);\n}\n\npublic function makePrimary(Post $post) \n{\n    // Handle         \n    // ...\n    return response('Done');\n} Let's examine a more practical example. Let's use the Post repository we defined above: Route wrapping: The $wrap argument determines whether your route should be wrapped with the default middlewares,\ncontrollers namespace, and prefix your routes with the repository's base (i.e., /api/restify/posts/). use App\\Restify\\Repository;\n\nclass PostRepository extends Repository\n{\n   public static function routes(\\Illuminate\\Routing\\Router $router, $attributes = [], $wrap = true)\n   {\n       $router->get('/{id}/kpi', 'PostController@kpi'); // /api/restify/posts/1/kpi\n   }\n} At this moment Restify is building the new route as a child of the posts, so it has the following route for instance: GET: /api/restify/posts/{id}/kpi This route is pointing to the PostsController@kpi. Let's define it: \u003C?php\n\nnamespace App\\Http\\Controllers;\n\nuse Illuminate\\Http\\JsonResponse;\nuse Binaryk\\LaravelRestify\\Controllers\\RestController;\n\nclass PostController extends RestController\n{\n    /**\n     * Show the kpi for the given user.\n     *\n     * @param  int  $id\n     * @return JsonResponse\n     */\n    public function kpi($id)\n    {\n        //...\n\n        return $this->response();\n    }\n}",{"id":1367,"title":1368,"titles":1369,"content":1370,"level":199},"/docs/api/repositories-advanced#route-prefix","Route prefix",[61,1363],"As we saw in the example above, the route is a child of the current repository. However, you might want to\nhave a separate prefix occasionally, which could be outside the URI of the current repository. Restify provides an easy way to do this by\nadding a default value prefix for the second $attributes argument: /**\n * @param  \\Illuminate\\Routing\\Router  $router\n * @param $options\n */\npublic static function routes(Router $router, $attributes = ['prefix' => 'api',], $wrap = true)\n{\n    $router->get('hello-world', function () {\n        return 'Hello World';\n    });\n} Now, the generated route will look like this: GET: '/api/hello-world With api as a custom prefix.",{"id":1372,"title":1373,"titles":1374,"content":1375,"level":199},"/docs/api/repositories-advanced#route-middleware","Route middleware",[61,1363],"All routes declared in the routes method will have the same middlewares defined in your restify.middleware\nconfiguration file. Overriding default middlewares is straightforward with Restify: /**\n * @param  \\Illuminate\\Routing\\Router  $router\n * @param $attributes\n */\npublic static function routes(Router $router, $attributes = ['middleware' => [CustomMiddleware::class],], $wrap = true)\n{\n    $router->get('hello-world', function () {\n        return 'Hello World';\n    });\n} In that case, the single middleware of the route will be defined by the CustomMiddleware class.",{"id":1377,"title":1378,"titles":1379,"content":1380,"level":199},"/docs/api/repositories-advanced#route-namespace","Route Namespace",[61,1363],"By default, each route defined in the routes method will have the namespace AppRootNamespace\\Http\\Controllers. You\ncan override it easily by using namespace configuration key: /**\n * @param  \\Illuminate\\Routing\\Router  $router\n * @param $attributes\n */\npublic static function routes(Router $router, $attributes = ['namespace' => 'App\\Services',], $wrap = true)\n{\n    $router->get('hello-world', 'WorldController@hello');\n} Non-wrapped routes: When $wrap is false, your routes will only have the Route group $attributes, which means that no\nprefix, middleware, or namespace will be applied automatically, even if you defined them as default arguments in\nthe routes method. You should be careful about this behavior.",{"id":1382,"title":1383,"titles":1384,"content":1385,"level":188},"/docs/api/repositories-advanced#advanced-field-methods","Advanced Field Methods",[61],"While the basic fields() method works for most cases, you can define different fields for different operations: class PostRepository extends Repository\n{\n    // Default fields used for all operations\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('excerpt'),\n            field('published_at'),\n            field('created_at'),\n            field('updated_at'),\n        ];\n    }\n    \n    // Lighter fields for listing operations\n    public function fieldsForIndex(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('excerpt'),\n            field('published_at'),\n        ];\n    }\n    \n    // Full detail fields for individual resources\n    public function fieldsForShow(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('excerpt'),\n            field('published_at'),\n            field('author_name', fn() => $this->author->name),\n        ];\n    }\n    \n    // Only allow certain fields to be created\n    public function fieldsForStore(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required(),\n            field('content')->required(),\n            field('excerpt'),\n        ];\n    }\n    \n    // Only allow certain fields to be updated\n    public function fieldsForUpdate(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('excerpt'),\n            field('published_at'),\n        ];\n    }\n}",{"id":1387,"title":1388,"titles":1389,"content":1390,"level":199},"/docs/api/repositories-advanced#field-method-priority","Field Method Priority",[61,1383],"Restify uses this priority order when determining which fields to use: Operation-specific method (fieldsForIndex, fieldsForShow, etc.) - Highest priorityDefault fields method (fields) - Fallback",{"id":1392,"title":1393,"titles":1394,"content":1395,"level":188},"/docs/api/repositories-advanced#public-repositories","Public Repositories",[61],"Sometimes you need to expose certain repositories without authentication (e.g., for a public blog or documentation site). Use public repositories carefully. Consider using the [serializer](/docs/api/serializer) for custom public endpoints instead. class PostRepository extends Repository\n{\n    // Allow public access\n    public static bool $public = true;\n    \n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('published_at'),\n        ];\n    }\n}",{"id":1397,"title":1398,"titles":1399,"content":1400,"level":199},"/docs/api/repositories-advanced#public-repository-setup","Public Repository Setup",[61,1393],"1. Update your global gate to allow null users: protected function gate(): void\n{\n    Gate::define('viewRestify', function ($user = null) {\n        if (is_null($user)) {\n            return true; // Allow public access\n        }\n        \n        return in_array($user->email, [...]);\n    });\n} 2. Update your policies to allow null users: public function allowRestify(User $user = null): bool\n{\n    return true; // Allow all users (authenticated or not)\n}\n\npublic function view(User $user = null, Post $post): bool\n{\n    return $post->status === 'published'; // Only show published posts\n}",{"id":1402,"title":1161,"titles":1403,"content":224,"level":188},"/docs/api/repositories-advanced#repository-collections-and-transforms",[61],{"id":1405,"title":1406,"titles":1407,"content":1408,"level":199},"/docs/api/repositories-advanced#transform-collections-before-serialization","Transform Collections Before Serialization",[61,1161],"class PostRepository extends Repository\n{\n    public function indexCollection(RestifyRequest $request, Collection $items): Collection\n    {\n        // Filter out unpublished posts\n        return $items->filter(function ($post) {\n            return $post->published_at \u003C= now();\n        });\n    }\n}",{"id":1410,"title":1197,"titles":1411,"content":1412,"level":199},"/docs/api/repositories-advanced#custom-serialization",[61,1161],"Take complete control over how your resources are serialized: class PostRepository extends Repository\n{\n    public function serializeForIndex(RestifyRequest $request): array\n    {\n        return [\n            'id' => $this->id,\n            'title' => $this->title,\n            'excerpt' => Str::limit($this->content, 100),\n            'read_time' => $this->calculateReadTime(),\n            'url' => route('posts.show', $this->slug),\n        ];\n    }\n    \n    public function serializeForShow(RestifyRequest $request): array\n    {\n        $data = parent::serializeForShow($request);\n        \n        // Add computed fields\n        $data['computed'] = [\n            'word_count' => str_word_count(strip_tags($this->content)),\n            'reading_time' => ceil(str_word_count($this->content) / 200),\n            'related_posts' => $this->getRelatedPosts(3),\n        ];\n        \n        return $data;\n    }\n}",{"id":1414,"title":1170,"titles":1415,"content":224,"level":188},"/docs/api/repositories-advanced#repository-labels-and-identifiers",[61],{"id":1417,"title":1418,"titles":1419,"content":1420,"level":199},"/docs/api/repositories-advanced#custom-labels","Custom Labels",[61,1170],"Customize how your repository appears in API documentation: class PostRepository extends Repository\n{\n    public static string $label = 'Blog Articles';\n    \n    // Or dynamically\n    public static function label(): string\n    {\n        return __('repository.posts');\n    }\n}",{"id":1422,"title":1423,"titles":1424,"content":1425,"level":199},"/docs/api/repositories-advanced#title-and-subtitle-fields","Title and Subtitle Fields",[61,1170],"class PostRepository extends Repository\n{\n    public static string $title = 'title'; // Default is 'id'\n    \n    public function title(): string\n    {\n        return $this->title ?: \"Post #{$this->id}\";\n    }\n    \n    public function subtitle(): ?string\n    {\n        return \"By {$this->author->name} on {$this->published_at->format('M j, Y')}\";\n    }\n}",{"id":1427,"title":1428,"titles":1429,"content":1430,"level":199},"/docs/api/repositories-advanced#custom-uri-keys","Custom URI Keys",[61,1170],"class PostRepository extends Repository\n{\n    // Use 'articles' instead of 'posts' in URLs\n    public static string $uriKey = 'articles';\n    \n    // Or dynamically\n    public static function uriKey(): string\n    {\n        return config('app.locale') === 'es' ? 'articulos' : 'articles';\n    }\n}",{"id":1432,"title":1433,"titles":1434,"content":1435,"level":188},"/docs/api/repositories-advanced#repository-lifecycle-and-events","Repository Lifecycle and Events",[61],"Hook into repository operations to perform additional logic:",{"id":1437,"title":1235,"titles":1438,"content":1439,"level":199},"/docs/api/repositories-advanced#single-resource-events",[61,1433],"use Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Cache;\n\nclass PostRepository extends Repository\n{\n    // Called after successfully creating a resource\n    public static function stored($model, $request)\n    {\n        Log::info(\"Post created: {$model->title}\");\n        Cache::tags(['posts'])->flush();\n        $model->searchable(); // Add to search index\n    }\n    \n    // Called after successfully updating a resource  \n    public static function updated($model, $request)\n    {\n        $dirty = $model->getDirty();\n        Log::info(\"Post updated: {$model->title}\", [\n            'changed_fields' => array_keys($dirty),\n        ]);\n        \n        if (isset($dirty['content']) || isset($dirty['title'])) {\n            $model->searchable(); // Re-index if content changed\n        }\n    }\n    \n    // Called after successfully deleting a resource\n    public static function deleted($status, $request)\n    {\n        if ($status) {\n            Cache::tags(['posts'])->flush();\n        }\n    }\n}",{"id":1441,"title":1240,"titles":1442,"content":1443,"level":199},"/docs/api/repositories-advanced#bulk-operation-events",[61,1433],"class PostRepository extends Repository\n{\n    public static function storedBulk(Collection $models, $request)\n    {\n        Log::info(\"Bulk created {$models->count()} posts\");\n        $models->searchable(); // Bulk index for search\n    }\n    \n    public static function updatedBulk(Collection $models, $request)\n    {\n        Cache::tags(['posts'])->flush();\n    }\n}",{"id":1445,"title":1446,"titles":1447,"content":1448,"level":199},"/docs/api/repositories-advanced#authorization-hooks","Authorization Hooks",[61,1433],"Custom authorization logic beyond policies: class PostRepository extends Repository\n{\n    public function allowToShow($request): self\n    {\n        if (!$this->model()->isPublished() && !$request->user()->isAdmin()) {\n            throw new AuthorizationException('Cannot view unpublished post');\n        }\n        \n        return $this;\n    }\n    \n    public function allowToStore(RestifyRequest $request, $payload = null): self\n    {\n        if ($request->user()->posts()->today()->count() >= 10) {\n            throw new AuthorizationException('Daily post limit reached');\n        }\n        \n        return $this;\n    }\n    \n    public function allowToUpdate(RestifyRequest $request, $payload = null): self\n    {\n        if ($this->model()->isPublished() && !$request->user()->isEditor()) {\n            throw new AuthorizationException('Cannot edit published posts');\n        }\n        \n        return $this;\n    }\n}",{"id":1450,"title":1451,"titles":1452,"content":1453,"level":188},"/docs/api/repositories-advanced#repository-lifecycle","Repository Lifecycle",[61],"Each repository has several lifecycle methods. The most useful is booted, which is called as soon as the repository is loaded: protected static function booted()\n{\n    // Initialization logic here\n} html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .svl4J, html code.shiki .svl4J{--shiki-light:#F76D47;--shiki-light-font-style:italic;--shiki-default:#F78C6C;--shiki-default-font-style:italic;--shiki-dark:#F78C6C;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sFweD, html code.shiki .sFweD{--shiki-light:#E2931D;--shiki-light-font-style:italic;--shiki-default:#FFCB6B;--shiki-default-font-style:italic;--shiki-dark:#FFCB6B;--shiki-dark-font-style:italic}",{"id":66,"title":65,"titles":1455,"content":1456,"level":182},[],"Generate Laravel Restify repositories with smart path detection and automatic relationship scaffolding using the restify:repository Artisan command. Laravel Restify provides powerful repository generation commands for both individual and bulk repository creation, with intelligent path detection and automatic relationship generation.",{"id":1458,"title":1459,"titles":1460,"content":1461,"level":188},"/docs/api/repository-generation#intelligent-path-detection","Intelligent Path Detection",[65],"The repository generator now automatically detects your project's repository organization pattern and creates new repositories in the appropriate location.",{"id":1463,"title":1464,"titles":1465,"content":1466,"level":199},"/docs/api/repository-generation#supported-patterns","Supported Patterns",[65,1459],"Grouped by Model - App/Restify/Users/UserRepository.phpDomain Driven - App/Restify/Domains/User/UserRepository.phpModule Based - App/Restify/Admin/UserRepository.phpFlat Structure - App/Restify/UserRepository.php (default)",{"id":1468,"title":354,"titles":1469,"content":1470,"level":199},"/docs/api/repository-generation#how-it-works",[65,1459],"When you run: php artisan restify:repository PostRepository The command will: First check the app/Restify directory for existing repositoriesIf none found in app/Restify, scan the entire app/ directoryAnalyze the location patterns of found repositoriesApply the same pattern to the new repositoryDisplay the detected pattern and target location This prioritization ensures that repositories in the standard App/Restify location are preferred over other locations.",{"id":1472,"title":1473,"titles":1474,"content":1475,"level":199},"/docs/api/repository-generation#example-output","Example Output",[65,1459],"$ php artisan restify:repository PostRepository\nDetected repository pattern: grouped-by-model\nRepository will be created in: App\\Restify\nRepository created successfully. If your project has UserRepository in App/Restify/Users/, the new PostRepository will be created in App/Restify/Posts/.",{"id":1477,"title":1478,"titles":1479,"content":1480,"level":188},"/docs/api/repository-generation#automatic-relationship-detection","Automatic Relationship Detection",[65],"When you run the repository generation command: php artisan restify:repository PostRepository The command will: Analyze your database schema for foreign key columnsGenerate regular fields in the fields() methodGenerate BelongsTo and HasMany relationships in a separate static include() method",{"id":1482,"title":1483,"titles":1484,"content":1485,"level":188},"/docs/api/repository-generation#generated-structure","Generated Structure",[65],"For a posts table with user_id and category_id columns, and a comments table with post_id, the generated repository will look like: \u003C?php\n\nnamespace App\\Restify;\n\nuse App\\Models\\Post;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Binaryk\\LaravelRestify\\Fields\\BelongsTo;\nuse Binaryk\\LaravelRestify\\Fields\\HasMany;\n\nclass PostRepository extends Repository\n{\n    public static string $model = Post::class;\n\n    public static function include(): array\n    {\n        return [\n            BelongsTo::make('user', UserRepository::class),\n            BelongsTo::make('category', CategoryRepository::class),\n            HasMany::make('comments', CommentRepository::class),\n        ];\n    }\n\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            id(),\n            field('title')->required(),\n            field('content')->textarea()->required(),\n            field('created_at')->datetime()->readonly(),\n            field('updated_at')->datetime()->readonly(),\n        ];\n    }\n}",{"id":1487,"title":354,"titles":1488,"content":224,"level":188},"/docs/api/repository-generation#how-it-works-1",[65],{"id":1490,"title":1491,"titles":1492,"content":1493,"level":199},"/docs/api/repository-generation#belongsto-detection","BelongsTo Detection",[65,354],"Columns ending with _id (except id itself) are detected as BelongsTo relationshipsThe relationship name is derived from the column name (e.g., user_id → user)The command attempts to find the related repository class automatically",{"id":1495,"title":1496,"titles":1497,"content":1498,"level":199},"/docs/api/repository-generation#hasmany-detection","HasMany Detection",[65,354],"The command scans other tables for foreign keys pointing to the current modelFor example, if comments table has post_id, it generates HasMany::make('comments')Repository classes are automatically resolved when possible",{"id":1500,"title":1501,"titles":1502,"content":1503,"level":199},"/docs/api/repository-generation#repository-resolution","Repository Resolution",[65,354],"The command searches for repository classes in these locations: App\\Restify\\{Model}RepositoryApp\\Http\\Restify\\{Model}Repository If a repository isn't found, the relationship is still generated without the repository parameter, allowing Laravel Restify to auto-resolve it.",{"id":1505,"title":404,"titles":1506,"content":1507,"level":188},"/docs/api/repository-generation#benefits",[65],"Separation of Concerns: Fields and relationships are kept in separate methodsClean Code: Foreign key fields are not duplicated in the fields arrayAutomatic Detection: Reduces manual work when setting up repositoriesFollows Best Practices: Uses the static include() method as recommended in Laravel Restify documentation",{"id":1509,"title":1510,"titles":1511,"content":1512,"level":188},"/docs/api/repository-generation#customization","Customization",[65],"You can always modify the generated relationships after creation: public static function include(): array\n{\n    return [\n        BelongsTo::make('user', UserRepository::class)->searchable('name'),\n        BelongsTo::make('category')->nullable(),\n        HasMany::make('comments')->sortable('created_at'),\n        \n        // Add more relationships manually\n        MorphMany::make('tags'),\n        BelongsToMany::make('subscribers')->withPivot('subscribed_at'),\n    ];\n}",{"id":1514,"title":1515,"titles":1516,"content":1517,"level":188},"/docs/api/repository-generation#override-confirmation","Override Confirmation",[65],"If a repository already exists at the target location, the command will ask for confirmation before overriding: $ php artisan restify:repository UserRepository\nDetected repository pattern: flat\nRepository will be created in: App\\Restify\nRepository already exists at: /path/to/app/Restify/UserRepository.php\nDo you want to override it? (yes/no) [no]: You can skip this confirmation by using the --force option: php artisan restify:repository UserRepository --force",{"id":1519,"title":1520,"titles":1521,"content":1522,"level":188},"/docs/api/repository-generation#disabling-automatic-generation","Disabling Automatic Generation",[65],"If you prefer to handle relationships manually, use the --no-fields option: php artisan restify:repository PostRepository --no-fields This will generate a repository with only the id() field and no relationships.",{"id":1524,"title":252,"titles":1525,"content":1526,"level":188},"/docs/api/repository-generation#bulk-repository-generation",[65],"For new projects or when you need to generate repositories for multiple models at once, Laravel Restify provides a bulk generation command that can analyze all your models and create repositories automatically.",{"id":1528,"title":1529,"titles":1530,"content":1531,"level":199},"/docs/api/repository-generation#basic-usage","Basic Usage",[65,252],"php artisan restify:generate:repositories This command will: Discover all models in your application automaticallyAnalyze database schema to generate appropriate field definitionsShow a detailed preview of what will be generatedAsk for confirmation before creating any filesGenerate repositories with proper field mappings",{"id":1533,"title":1534,"titles":1535,"content":1536,"level":199},"/docs/api/repository-generation#interactive-preview","Interactive Preview",[65,252],"Before generating any files, the command shows a comprehensive preview: 📋 Preview of repositories to be generated:\n═══════════════════════════════════════════════════════\n\n🔍 Found 3 models:\n   • User (table: users, 8 fields)\n   • Post (table: posts, 6 fields)\n   • Comment (table: comments, 4 fields)\n\n📂 Repository configuration:\n   Structure: flat\n   Base namespace: App\\Restify\n   Force overwrite: No\n\n📄 Repositories that will be generated:\n   1. app/Restify/UserRepository.php\n   2. app/Restify/PostRepository.php\n   3. app/Restify/CommentRepository.php\n\n📝 Sample repository preview:\n   ┌─────────────────────────────────────────────────────┐\n   │ class UserRepository extends Repository             │\n   │ {                                                   │\n   │     public static string $model = User::class;     │\n   │                                                     │\n   │     public function fields(RestifyRequest $request) │\n   │     {                                               │\n   │         return [                                    │\n   │             id(),                                   │\n   │             field('name'),                          │\n   │             field('email')->email(),                │\n   │             field('created_at')->datetime()->readonly(), │\n   │             # ... 5 more fields                     │\n   │         ];                                          │\n   │     }                                               │\n   │ }                                                   │\n   └─────────────────────────────────────────────────────┘",{"id":1538,"title":1539,"titles":1540,"content":1541,"level":199},"/docs/api/repository-generation#command-options","Command Options",[65,252],"OptionDescriptionExample--forceOverwrite existing repositories--force--skip-previewSkip preview and generate immediately--skip-preview--structure=flat|domainsChoose repository structure--structure=domains--only=Model1,Model2Only generate for specific models--only=User,Post--except=Model1,Model2Exclude specific models--except=User,Post",{"id":1543,"title":1544,"titles":1545,"content":1546,"level":199},"/docs/api/repository-generation#repository-structures","Repository Structures",[65,252],"The command supports two organizational patterns:",{"id":1548,"title":1549,"titles":1550,"content":1551,"level":834},"/docs/api/repository-generation#flat-structure-default","Flat Structure (Default)",[65,252,1544],"app/Restify/\n├── UserRepository.php\n├── PostRepository.php\n└── CommentRepository.php",{"id":1553,"title":1554,"titles":1555,"content":1556,"level":834},"/docs/api/repository-generation#domains-structure","Domains Structure",[65,252,1544],"app/Restify/Domains/\n├── User/\n│   └── UserRepository.php\n├── Post/\n│   └── PostRepository.php\n└── Comment/\n    └── CommentRepository.php Choose the structure interactively or specify with --structure: # Interactive structure selection\nphp artisan restify:generate:repositories\n\n# Force domains structure\nphp artisan restify:generate:repositories --structure=domains",{"id":1558,"title":1559,"titles":1560,"content":1561,"level":199},"/docs/api/repository-generation#field-type-detection","Field Type Detection",[65,252],"The command automatically maps database columns to appropriate Restify fields: Database TypeRestify FieldExamplestring, varcharfield()field('name')Email columnsfield()->email()field('email')->email()Password columnsfield()->password()->storable()field('password')->password()->storable()text, longtextfield()->textarea()field('description')->textarea()integer, bigintfield()->number()field('count')->number()boolean, tinyintfield()->boolean()field('is_active')->boolean()datefield()->date()field('birth_date')->date()datetime, timestampfield()->datetime()field('created_at')->datetime()decimal, floatfield()->number()field('price')->number()jsonfield()->json()field('metadata')->json()",{"id":1563,"title":1564,"titles":1565,"content":1566,"level":199},"/docs/api/repository-generation#special-field-handling","Special Field Handling",[65,252],"Timestamps: created_at, updated_at, deleted_at → automatically marked as readonly()Foreign Keys: Columns ending with _id are excluded (handled as relationships)Email Fields: Columns containing \"email\" → mapped to email() fieldPassword Fields: Columns containing \"password\" → mapped to password()->storable()",{"id":1568,"title":1569,"titles":1570,"content":224,"level":199},"/docs/api/repository-generation#filtering-models","Filtering Models",[65,252],{"id":1572,"title":1573,"titles":1574,"content":1575,"level":834},"/docs/api/repository-generation#generate-only-specific-models","Generate Only Specific Models",[65,252,1569],"php artisan restify:generate:repositories --only=User,Post",{"id":1577,"title":1578,"titles":1579,"content":1580,"level":834},"/docs/api/repository-generation#exclude-specific-models","Exclude Specific Models",[65,252,1569],"php artisan restify:generate:repositories --except=User,Post This is useful when you want to exclude certain models like: Third-party package models (Spatie permissions, etc.)Internal system modelsModels that don't need API endpoints",{"id":1582,"title":1583,"titles":1584,"content":224,"level":199},"/docs/api/repository-generation#examples","Examples",[65,252],{"id":1586,"title":1587,"titles":1588,"content":1589,"level":834},"/docs/api/repository-generation#quick-setup-for-new-project","Quick Setup for New Project",[65,252,1583],"# Generate all repositories with preview\nphp artisan restify:generate:repositories",{"id":1591,"title":1592,"titles":1593,"content":1594,"level":834},"/docs/api/repository-generation#production-setup-with-domains-structure","Production Setup with Domains Structure",[65,252,1583],"# Generate with domains structure, skip preview\nphp artisan restify:generate:repositories \\\n  --structure=domains \\\n  --skip-preview \\\n  --force",{"id":1596,"title":1597,"titles":1598,"content":1599,"level":834},"/docs/api/repository-generation#selective-generation","Selective Generation",[65,252,1583],"# Only generate for core models\nphp artisan restify:generate:repositories \\\n  --only=User,Post,Comment,Category \\\n  --structure=flat",{"id":1601,"title":1602,"titles":1603,"content":1604,"level":834},"/docs/api/repository-generation#excluding-system-models","Excluding System Models",[65,252,1583],"# Generate all except system models\nphp artisan restify:generate:repositories \\\n  --except=PersonalAccessToken,PasswordReset,Permission,Role",{"id":1606,"title":1607,"titles":1608,"content":1609,"level":199},"/docs/api/repository-generation#generated-repository-structure","Generated Repository Structure",[65,252],"Each generated repository includes: \u003C?php\n\nnamespace App\\Restify;\n\nuse App\\Models\\User;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Binaryk\\LaravelRestify\\Repositories\\Repository;\n\nclass UserRepository extends Repository\n{\n    public static string $model = User::class;\n\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            id(),\n            field('name'),\n            field('email')->email(),\n            field('email_verified_at')->datetime()->readonly(),\n            field('created_at')->datetime()->readonly(),\n            field('updated_at')->datetime()->readonly(),\n        ];\n    }\n}",{"id":1611,"title":1612,"titles":1613,"content":1614,"level":199},"/docs/api/repository-generation#integration-with-individual-generation","Integration with Individual Generation",[65,252],"The bulk generation command works seamlessly with the individual repository command. You can: Use bulk generation to create the initial repositoriesUse individual generation to add new repositories as neededBoth commands respect the same path detection and organizational patterns",{"id":1616,"title":1617,"titles":1618,"content":1619,"level":199},"/docs/api/repository-generation#best-practices","Best Practices",[65,252],"Review Generated Files: Always review generated repositories before committingAdd Relationships: The bulk generator focuses on fields; add relationships manuallyConfigure Authorization: Set up policies for the generated repositoriesTest Endpoints: Verify that all generated endpoints work as expected",{"id":1621,"title":1622,"titles":1623,"content":224,"level":199},"/docs/api/repository-generation#troubleshooting","Troubleshooting",[65,252],{"id":1625,"title":1626,"titles":1627,"content":1628,"level":834},"/docs/api/repository-generation#no-models-found","No Models Found",[65,252,1622],"If the command reports \"No models found\", ensure: Models are in the app/Models directory (or app/ for older Laravel versions)Models extend Illuminate\\Database\\Eloquent\\ModelModels are not in excluded paths (Http, Console, Exceptions, Providers, Restify)",{"id":1630,"title":1631,"titles":1632,"content":1633,"level":834},"/docs/api/repository-generation#field-detection-issues","Field Detection Issues",[65,252,1622],"If fields are missing or incorrect: Ensure database tables exist and are migratedCheck that model $table property is set correctlyVerify database connection is working",{"id":1635,"title":1636,"titles":1637,"content":1638,"level":834},"/docs/api/repository-generation#permission-errors","Permission Errors",[65,252,1622],"If you encounter permission errors during generation: Ensure the app/Restify directory is writableCheck file permissions in your Laravel application html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":70,"title":69,"titles":1640,"content":1641,"level":182},[],"Learn how to declare and customize model attribute fields in Laravel Restify using a fluent API with built-in validation, mutators, and computed values. A field is basically the model's attribute representation.",{"id":1643,"title":1644,"titles":1645,"content":1646,"level":188},"/docs/api/fields#declaration","Declaration",[69],"Each Field generally extends the Binaryk\\LaravelRestify\\Fields\\Field class from the Laravel Restify. This class ships\na fluent API for a variety of mutators, interceptors and validators. To add a field to a repository, we can simply add it to the repository's fields method. Typically, fields may be created\nusing their static new or make method. The first argument is always the attribute name and usually matches the database column. use Illuminate\\Support\\Facades\\Hash;\nuse Binaryk\\LaravelRestify\\Fields\\Field;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\n\npublic function fields(RestifyRequest $request)\n{\n    return [\n        Field::make('name')->required(),\n        \n        Field::make('email')->required()->storingRules('unique:users')->messages([\n            'required' => 'This field is required.',\n        ]),\n    ];\n}",{"id":1648,"title":1649,"titles":1650,"content":1651,"level":199},"/docs/api/fields#field-helper","field helper",[69,1644],"Instead of using the Field class, you can use the field helper. For example:field('email')",{"id":1653,"title":1654,"titles":1655,"content":1656,"level":199},"/docs/api/fields#computed-field","Computed field",[69,1644],"The second optional argument is a callback or invokable, and it represents the displayable value of the field either in show or index requests. field('name', fn() => 'John Doe') The field above will always return the name value as John Doe. The field is still writeable, so you can update or create an entity by using it.",{"id":1658,"title":1659,"titles":1660,"content":1661,"level":199},"/docs/api/fields#readonly-field","Readonly field",[69,1644],"If you don't want a field to be writeable you can mark it as readonly: field('title')->readonly() The readonly accepts a request as well as you can use: field('title')->readonly(fn($request) => $request->user()->isGuest())",{"id":1663,"title":1664,"titles":1665,"content":1666,"level":199},"/docs/api/fields#virtual-field","Virtual field",[69,1644],"A virtual field, is a field that's computed and readonly. field('name', fn() => \"$this->first_name $this->last_name\")->readonly()",{"id":1668,"title":32,"titles":1669,"content":1670,"level":188},"/docs/api/fields#authorization",[69],"The Field class provides a few methods in order to authorize certain actions. Each authorization method accepts a Closure that\nshould return true\nor false. The Closure will receive the incoming \\Illuminate\\Http\\Request request.",{"id":1672,"title":1673,"titles":1674,"content":1675,"level":199},"/docs/api/fields#can-see","Can see",[69,32],"Sometimes, you may want to hide certain fields from a group of users. You may easily accomplish this by chaining\nthe canSee: public function fields(RestifyRequest $request)\n{\n   return [\n       field('role_id')->canSee(fn($request) => $request->user()->isAdmin())\n   ];\n}",{"id":1677,"title":1678,"titles":1679,"content":1680,"level":199},"/docs/api/fields#can-store","Can store",[69,32],"The can store closure: public function fields(RestifyRequest $request)\n{\n    return [\n        field('role_id')->canStore(fn($request) => $request->user()->isAdmin())\n}",{"id":1682,"title":1683,"titles":1684,"content":1685,"level":199},"/docs/api/fields#can-update","Can update",[69,32],"The can update closure: public function fields(RestifyRequest $request)\n{\n    return [\n        field('role_id')->canUpdate(fn($request) => $request->user()->isAdmin())\n    ];\n}",{"id":1687,"title":1688,"titles":1689,"content":1690,"level":199},"/docs/api/fields#can-patch","Can patch",[69,32],"You can authorize PATCH operations specifically: public function fields(RestifyRequest $request)\n{\n    return [\n        field('status')->canPatch(fn($request) => $request->user()->can('patch-status'))\n    ];\n}",{"id":1692,"title":1693,"titles":1694,"content":1695,"level":199},"/docs/api/fields#can-update-bulk","Can update bulk",[69,32],"For bulk update operations, you can control authorization: public function fields(RestifyRequest $request)\n{\n    return [\n        field('priority')->canUpdateBulk(fn($request) => $request->user()->isAdmin())\n    ];\n}",{"id":1697,"title":1698,"titles":1699,"content":1700,"level":188},"/docs/api/fields#bulk-operations","Bulk Operations",[69],"Laravel Restify provides specialized methods for handling bulk operations (creating or updating multiple records at once). Fields have specific callbacks and validation rules for these scenarios.",{"id":1702,"title":1703,"titles":1704,"content":1705,"level":199},"/docs/api/fields#bulk-visibility-control","Bulk Visibility Control",[69,1698],"You can control whether fields are visible during bulk operations: public function fields(RestifyRequest $request)\n{\n    return [\n        field('title')->showOnStoreBulk(true)->showOnUpdateBulk(false),\n        field('slug')->hideFromStoreBulk(), // Not editable during bulk creation\n    ];\n} The available visibility methods for bulk operations are: isShownOnStore() - Check if field is shown during single storeisShownOnStoreBulk() - Check if field is shown during bulk storeisShownOnUpdate() - Check if field is shown during single updateisShownOnUpdateBulk() - Check if field is shown during bulk update",{"id":1707,"title":1708,"titles":1709,"content":1710,"level":199},"/docs/api/fields#bulk-authorization","Bulk Authorization",[69,1698],"Use specialized authorization methods for bulk operations: public function fields(RestifyRequest $request)\n{\n    return [\n        field('status')\n            ->canStore(fn($request) => $request->user()->isAdmin())\n            ->canUpdateBulk(fn($request) => $request->user()->isSuperAdmin()),\n    ];\n}",{"id":1712,"title":1559,"titles":1713,"content":1714,"level":188},"/docs/api/fields#field-type-detection",[69],"Restify includes an intelligent field type detection system that automatically infers the appropriate data type for fields based on various factors. This is particularly useful for API schema generation and MCP integration.",{"id":1716,"title":1717,"titles":1718,"content":1719,"level":199},"/docs/api/fields#automatic-type-detection","Automatic Type Detection",[69,1559],"The guessFieldType() method analyzes fields using multiple strategies: $field = field('email')->rules('required', 'email');\n$type = $field->guessFieldType(); // Returns: 'string'\n\n$field = field('is_active')->rules('boolean'); \n$type = $field->guessFieldType(); // Returns: 'boolean'\n\n$field = field('age')->rules('integer', 'min:0');\n$type = $field->guessFieldType(); // Returns: 'number'",{"id":1721,"title":1722,"titles":1723,"content":1724,"level":199},"/docs/api/fields#detection-strategies","Detection Strategies",[69,1559],"The system uses three detection strategies in order of priority: Field Class Detection - Analyzes the field class name (File, Image, Boolean, etc.)Validation Rules Detection - Examines validation rules (email, boolean, integer, etc.)Attribute Name Patterns - Looks for common naming patterns",{"id":1726,"title":1727,"titles":1728,"content":1729,"level":834},"/docs/api/fields#field-class-patterns","Field Class Patterns",[69,1559,1722],"File::make('avatar')->guessFieldType(); // 'string'\nImage::make('photo')->guessFieldType(); // 'string'  \nBooleanField::make('active')->guessFieldType(); // 'boolean'",{"id":1731,"title":1732,"titles":1733,"content":1734,"level":834},"/docs/api/fields#validation-rule-patterns","Validation Rule Patterns",[69,1559,1722],"field('email')->rules('email')->guessFieldType(); // 'string'\nfield('count')->rules('integer')->guessFieldType(); // 'number'\nfield('tags')->rules('array')->guessFieldType(); // 'array'",{"id":1736,"title":1737,"titles":1738,"content":1739,"level":834},"/docs/api/fields#attribute-name-patterns","Attribute Name Patterns",[69,1559,1722],"field('is_featured')->guessFieldType(); // 'boolean' (is_ prefix)\nfield('user_id')->guessFieldType(); // 'number' (_id suffix)\nfield('created_at')->guessFieldType(); // 'string' (_at suffix)\nfield('settings_json')->guessFieldType(); // 'array' (_json suffix)",{"id":1741,"title":1742,"titles":1743,"content":1744,"level":199},"/docs/api/fields#computed-field-detection","Computed Field Detection",[69,1559],"You can check if a field is computed (virtual/calculated): $field = field('full_name', fn() => \"$this->first_name $this->last_name\");\n$isComputed = $field->computed(); // Returns: true",{"id":1746,"title":102,"titles":1747,"content":1748,"level":188},"/docs/api/fields#sorting",[69],"Fields can be made sortable, allowing API consumers to order results by field values.",{"id":1750,"title":1751,"titles":1752,"content":1753,"level":199},"/docs/api/fields#making-fields-sortable","Making Fields Sortable",[69,102],"To make a field sortable, chain the sortable() method: public function fields(RestifyRequest $request)\n{\n    return [\n        field('name')->sortable(),\n        field('email')->sortable(),\n        field('created_at')->sortable(),\n        field('is_active')->sortable(),\n    ];\n}",{"id":1755,"title":1756,"titles":1757,"content":1758,"level":199},"/docs/api/fields#sortable-column-configuration","Sortable Column Configuration",[69,102],"By default, the field's attribute name is used as the sortable column. You can specify a different column: field('full_name')->sortable('name'), // Use 'name' column for 'full_name' field",{"id":1760,"title":1761,"titles":1762,"content":1763,"level":199},"/docs/api/fields#disabling-sorting","Disabling Sorting",[69,102],"You can disable sorting for a field that was previously made sortable: field('sensitive_data')->sortable(false),",{"id":1765,"title":1766,"titles":1767,"content":1768,"level":199},"/docs/api/fields#conditional-sorting","Conditional Sorting",[69,102],"Make fields conditionally sortable based on request context: field('internal_score')->sortable(fn($request) => $request->user()->isAdmin()),",{"id":1770,"title":1771,"titles":1772,"content":1773,"level":199},"/docs/api/fields#using-sortable-fields","Using Sortable Fields",[69,102],"Once fields are marked as sortable, API consumers can use them in sort requests: GET /api/restify/users?sort=name\nGET /api/restify/users?sort=-created_at  # Descending\nGET /api/restify/users?sort=name,-created_at  # Multiple fields",{"id":1775,"title":1776,"titles":1777,"content":1778,"level":188},"/docs/api/fields#matching","Matching",[69],"Fields can be made matchable, allowing API consumers to filter results using query parameters.",{"id":1780,"title":1781,"titles":1782,"content":1783,"level":199},"/docs/api/fields#making-fields-matchable","Making Fields Matchable",[69,1776],"Use the matchable() method or convenient aliases: public function fields(RestifyRequest $request)\n{\n    return [\n        field('name')->matchableText(),              // Text matching with LIKE\n        field('email')->matchable('users.email'),   // Custom column - users table email\n        field('status')->matchableText(),            // Text matching\n        field('is_active')->matchableBool(),         // Boolean matching\n        field('age')->matchableInteger(),            // Integer matching\n        field('created_at')->matchableDatetime(),    // Date matching\n        field('price')->matchableBetween(),          // Range matching\n        field('tags')->matchableArray(),             // Array/IN matching\n    ];\n}",{"id":1785,"title":1786,"titles":1787,"content":1788,"level":199},"/docs/api/fields#using-matchable-fields","Using Matchable Fields",[69,1776],"Once fields are marked as matchable, API consumers can filter using query parameters: GET /api/restify/posts?title=Laravel         # Text matching\nGET /api/restify/posts?is_active=true        # Boolean matching\nGET /api/restify/posts?user_id=5             # Integer matching\nGET /api/restify/posts?created_at=2023-12-01 # Date matching\nGET /api/restify/posts?price=100,500         # Range matching\nGET /api/restify/posts?tags=php,laravel      # Array matching\n\n# Negation (prefix with -)\nGET /api/restify/posts?-status=draft         # Exclude drafts\nGET /api/restify/posts?-is_active=true       # Inactive posts\n\n# Null checks\nGET /api/restify/posts?description=null      # Posts with no description",{"id":1790,"title":1791,"titles":1792,"content":1793,"level":199},"/docs/api/fields#match-types-reference","Match Types Reference",[69,1776],"AliasTypeExample UsageQuery BehaviormatchableText()text?name=johnWHERE name LIKE '%john%'matchableBool()boolean?is_active=trueWHERE is_active = 1matchableInteger()integer?user_id=5WHERE user_id = 5matchableDatetime()datetime?created_at=2023-12-01WHERE DATE(created_at) = '2023-12-01'matchableBetween()between?price=100,500WHERE price BETWEEN 100 AND 500matchableArray()array?tags=php,laravelWHERE tags IN ('php', 'laravel')",{"id":1795,"title":1796,"titles":1797,"content":1798,"level":199},"/docs/api/fields#advanced-matchable-configuration","Advanced Matchable Configuration",[69,1776],"The matchable() method is flexible and accepts multiple types of arguments for advanced filtering scenarios:",{"id":1800,"title":1801,"titles":1802,"content":1803,"level":834},"/docs/api/fields#basic-usage-no-arguments","Basic Usage (No Arguments)",[69,1776,1796],"When called without arguments, matchable() enables text-based matching using the field's attribute name: field('title')->matchable(), // Enables text matching on 'title' column",{"id":1805,"title":1806,"titles":1807,"content":1808,"level":834},"/docs/api/fields#custom-column","Custom Column",[69,1776,1796],"Specify a different database column for matching: field('display_name')->matchable('users.name'), // Match against 'users.name' column",{"id":1810,"title":1811,"titles":1812,"content":1813,"level":834},"/docs/api/fields#custom-match-type","Custom Match Type",[69,1776,1796],"Specify both column and match type: field('status')->matchable('posts.status', 'text'), // Custom column with text matching\nfield('priority')->matchable('priority', 'integer'), // Integer matching",{"id":1815,"title":1816,"titles":1817,"content":1818,"level":834},"/docs/api/fields#closure-based-matching","Closure-based Matching",[69,1776,1796],"For complex filtering logic, pass a closure that receives the request, query builder, and value: field('title')->matchable(function ($request, $query, $value) {\n    // Custom search logic - case insensitive partial matching\n    $query->where('title', 'like', \"%{$value}%\");\n}),\n\nfield('content')->matchable(function ($request, $query, $value) {\n    // Full-text search across multiple columns\n    $query->whereRaw(\"MATCH(title, content) AGAINST(? IN BOOLEAN MODE)\", [$value]);\n}),\n\nfield('location')->matchable(function ($request, $query, $value) {\n    // Complex geographical search\n    [$lat, $lng, $radius] = explode(',', $value);\n    $query->whereRaw(\n        'ST_Distance_Sphere(POINT(longitude, latitude), POINT(?, ?)) \u003C= ?',\n        [$lng, $lat, $radius * 1000]\n    );\n}),",{"id":1820,"title":1821,"titles":1822,"content":1823,"level":834},"/docs/api/fields#custom-matchfilter-classes","Custom MatchFilter Classes",[69,1776,1796],"For reusable complex filtering logic, create custom MatchFilter classes: use Binaryk\\LaravelRestify\\Filters\\MatchFilter;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\nclass CustomTitleFilter extends MatchFilter\n{\n    public function __construct()\n    {\n        parent::__construct();\n        $this->setColumn('title'); // Set the column to filter on\n    }\n\n    public function filter(RestifyRequest $request, Builder|Relation $query, $value)\n    {\n        // Custom filtering logic: search for titles that start with the given value\n        $query->where('title', 'like', \"{$value}%\");\n        \n        return $query;\n    }\n} Then use the custom filter in your field definition: field('title')->matchable(new CustomTitleFilter()),",{"id":1825,"title":1826,"titles":1827,"content":1828,"level":834},"/docs/api/fields#invokable-classes","Invokable Classes",[69,1776,1796],"You can also use invokable classes for cleaner code organization: class SearchTitleFilter\n{\n    public function __invoke($request, $query, $value)\n    {\n        $query->where('title', 'like', \"%{$value}%\")\n              ->orWhere('description', 'like', \"%{$value}%\");\n    }\n}\n\n// Usage\nfield('search')->matchable(new SearchTitleFilter()),",{"id":1830,"title":1831,"titles":1832,"content":1833,"level":834},"/docs/api/fields#practical-examples","Practical Examples",[69,1776,1796],"E-commerce Product Search: field('search')->matchable(function ($request, $query, $value) {\n    $query->where(function ($q) use ($value) {\n        $q->where('name', 'like', \"%{$value}%\")\n          ->orWhere('description', 'like', \"%{$value}%\")\n          ->orWhere('sku', 'like', \"%{$value}%\");\n    });\n}), Date Range Filtering: field('date_range')->matchable(function ($request, $query, $value) {\n    [$start, $end] = explode(',', $value);\n    $query->whereBetween('created_at', [$start, $end]);\n}), Tag-based Filtering: field('tags')->matchable(function ($request, $query, $value) {\n    $tags = explode(',', $value);\n    $query->whereHas('tags', function ($q) use ($tags) {\n        $q->whereIn('slug', $tags);\n    });\n}), Relationship Filtering: field('author')->matchable(function ($request, $query, $value) {\n    $query->whereHas('author', function ($q) use ($value) {\n        $q->where('name', 'like', \"%{$value}%\")\n          ->orWhere('email', 'like', \"%{$value}%\");\n    });\n}),",{"id":1835,"title":1836,"titles":1837,"content":1838,"level":188},"/docs/api/fields#searchable","Searchable",[69],"Fields can be made searchable, enabling them to respond to global search queries. This provides field-level control over search behavior while maintaining the simplicity of the global search API.",{"id":1840,"title":1841,"titles":1842,"content":1843,"level":199},"/docs/api/fields#making-fields-searchable","Making Fields Searchable",[69,1836],"To make a field searchable, chain the searchable() method: public function fields(RestifyRequest $request)\n{\n    return [\n        field('title')->searchable(),\n        field('description')->searchable(),\n        field('email')->searchable(),\n    ];\n} The searchable() method uses a unified flexible signature that accepts multiple arguments and works consistently across all field types: // Basic usage\nfield('title')->searchable(),\n\n// Custom column\nfield('name')->searchable('users.full_name'),\n\n// With optional type\nfield('price')->searchable('products.price', 'numeric'),\n\n// Multiple attributes (especially useful for relationship fields like BelongsTo)\nBelongsTo::make('author')->searchable('name', 'email', 'username'),\n\n// Array of attributes (legacy support)\nBelongsTo::make('editor')->searchable(['users.name', 'users.email']),\n\n// Closure/callback\nfield('content')->searchable(function ($request, $query, $value) {\n    // Custom search logic\n}),\n\n// Custom filter instance\nfield('complex_search')->searchable(new CustomSearchFilter()),\n\n// Invokable class\nfield('tags')->searchable(new TagSearchHandler()),",{"id":1845,"title":1846,"titles":1847,"content":1848,"level":199},"/docs/api/fields#unified-method-signatures","Unified Method Signatures",[69,1836],"All searchable-related methods now use consistent signatures across regular fields and relationship fields: // All field types use the same signatures:\nsearchable(...$attributes)                    // Flexible variadic signature\nisSearchable(?RestifyRequest $request = null) // Optional request parameter\ngetSearchColumn(?RestifyRequest $request = null) // Optional request parameter\n\n// BelongsTo also provides relationship-specific method:\ngetSearchables(): array                       // Returns multiple searchable attributes",{"id":1850,"title":1851,"titles":1852,"content":1853,"level":199},"/docs/api/fields#using-searchable-fields","Using Searchable Fields",[69,1836],"Searchable fields respond to the standard search query parameter: GET /api/restify/posts?search=laravel This will search across all searchable fields for the term \"laravel\".",{"id":1855,"title":1856,"titles":1857,"content":224,"level":199},"/docs/api/fields#advanced-searchable-configuration","Advanced Searchable Configuration",[69,1836],{"id":1859,"title":1801,"titles":1860,"content":1861,"level":834},"/docs/api/fields#basic-usage-no-arguments-1",[69,1836,1856],"When called without arguments, searchable() applies standard search behavior using the field's attribute: field('title')->searchable(), // Searches the 'title' column with LIKE operator",{"id":1863,"title":1806,"titles":1864,"content":1865,"level":834},"/docs/api/fields#custom-column-1",[69,1836,1856],"Specify a different database column for searching: field('author_name')->searchable('users.name'), // Search in users.name column You can also specify multiple attributes for relationship fields (like BelongsTo): BelongsTo::make('author', UserRepository::class)->searchable('name', 'email'),",{"id":1867,"title":1868,"titles":1869,"content":1870,"level":834},"/docs/api/fields#closure-based-searching","Closure-based Searching",[69,1836,1856],"For custom search logic, pass a closure that receives the request, query builder, and search value: field('content')->searchable(function ($request, $query, $value) {\n    $query->where('title', 'LIKE', \"%{$value}%\")\n          ->orWhere('description', 'LIKE', \"%{$value}%\");\n}),",{"id":1872,"title":1873,"titles":1874,"content":1875,"level":834},"/docs/api/fields#custom-searchablefilter-classes","Custom SearchableFilter Classes",[69,1836,1856],"Create dedicated filter classes for complex search logic: field('complex_search')->searchable(new CustomContentSearchFilter), Where CustomContentSearchFilter extends SearchableFilter: use Binaryk\\LaravelRestify\\Filters\\SearchableFilter;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\n\nclass CustomContentSearchFilter extends SearchableFilter\n{\n    public function filter(RestifyRequest $request, $query, $value)\n    {\n        return $query->where(function ($q) use ($value) {\n            $q->where('title', 'LIKE', \"%{$value}%\")\n              ->orWhere('description', 'LIKE', \"%{$value}%\")\n              ->orWhere('tags', 'LIKE', \"%{$value}%\");\n        });\n    }\n}",{"id":1877,"title":1826,"titles":1878,"content":1879,"level":834},"/docs/api/fields#invokable-classes-1",[69,1836,1856],"For reusable search logic, use invokable classes: field('tags')->searchable(new TagSearchFilter), class TagSearchFilter\n{\n    public function __invoke($request, $query, $value)\n    {\n        $tags = explode(',', $value);\n        $query->whereHas('tags', function ($q) use ($tags) {\n            $q->whereIn('name', $tags);\n        });\n    }\n}",{"id":1881,"title":1831,"titles":1882,"content":1883,"level":834},"/docs/api/fields#practical-examples-1",[69,1836,1856],"Full-text Search: field('content')->searchable(function ($request, $query, $value) {\n    $query->whereFullText(['title', 'description'], $value);\n}), Multi-field Search: field('user_search')->searchable(function ($request, $query, $value) {\n    $query->where('name', 'LIKE', \"%{$value}%\")\n          ->orWhere('email', 'LIKE', \"%{$value}%\")\n          ->orWhere('phone', 'LIKE', \"%{$value}%\");\n}), Relationship Search: field('author')->searchable(function ($request, $query, $value) {\n    $query->whereHas('author', function ($q) use ($value) {\n        $q->where('name', 'like', \"%{$value}%\");\n    });\n}), JSON Search: field('metadata')->searchable(function ($request, $query, $value) {\n    $query->whereJsonContains('metadata->tags', $value);\n}),",{"id":1885,"title":1886,"titles":1887,"content":1888,"level":188},"/docs/api/fields#validation","Validation",[69],"There is a golden rule that says - catch the exception as soon as possible on its request way. Validations are the first bridge of your request information, so it would be a good start to validate your input. In this manner, you\ndon't have to worry about the payload anymore.",{"id":1890,"title":1891,"titles":1892,"content":1893,"level":199},"/docs/api/fields#attaching-rules","Attaching rules",[69,1886],"Validation rules could be added by chaining the rules method to\nattach validation rules to the field: field('email')->rules('required', 'email'), Of course, if you are leveraging Laravel's support\nfor validation rule objects, you may attach those to the resources\nas well: Field::new('email')->rules('required', new CustomRule), Additionally, you may use custom Closure rules\nto validate your resource fields: Field::new('email')->rules('required', function($attribute, $value, $fail) {\n    if (strtolower($value) !== $value) {\n        return $fail('The '.$attribute.' field must be lowercase.');\n    }\n}), Considering the required rule is very often used, Restify provides a required() validation\nhelper: field('email')->required() These rules will be applied for all the update and store requests.",{"id":1895,"title":1896,"titles":1897,"content":1898,"level":199},"/docs/api/fields#storing-rules","Storing Rules",[69,1886],"If you would like to define more specific rules that only apply when a resource is being stored, you might want to use\nthe storingRules method: Field::new('email')\n    ->rules('required', 'email', 'max:255')\n    ->storingRules('unique:users,email'); Considering the fact that Restify concatenates rules provided by the rules() method, the entire validation for a POST request on\nthis repository will look like this: $request->validate([\n    'email' => ['required', 'email', 'max:255', 'unique:users,email']\n]);",{"id":1900,"title":1901,"titles":1902,"content":1903,"level":199},"/docs/api/fields#updating-rules","Updating Rules",[69,1886],"Similarly, if you would like to define rules that only apply when a resource is being updated, you may use\nthe updatingRules method. Field::new('email')->updatingRules('required', 'email');",{"id":1905,"title":1906,"titles":1907,"content":1908,"level":199},"/docs/api/fields#bulk-rules","Bulk Rules",[69,1886],"For bulk operations, you can specify validation rules that apply only during bulk store or bulk update operations:",{"id":1910,"title":1911,"titles":1912,"content":1913,"level":834},"/docs/api/fields#store-bulk-rules","Store Bulk Rules",[69,1886,1906],"Rules that apply only during bulk store operations: Field::new('email')\n    ->rules('required', 'email')\n    ->storeBulkRules('unique:users,email');",{"id":1915,"title":1916,"titles":1917,"content":1918,"level":834},"/docs/api/fields#update-bulk-rules","Update Bulk Rules",[69,1886,1906],"Rules that apply only during bulk update operations: Field::new('email')\n    ->rules('email')\n    ->updateBulkRules('required');",{"id":1920,"title":1921,"titles":1922,"content":1923,"level":199},"/docs/api/fields#store-rules-alias","Store Rules Alias",[69,1886],"You can also use storeRules() as an alias for storingRules(): Field::new('email')->storeRules('unique:users,email');",{"id":1925,"title":1926,"titles":1927,"content":1928,"level":188},"/docs/api/fields#interceptors","Interceptors",[69],"Sometimes you might want to take control over certain Field actions. That's why the Field class exposes a lot of chained methods you can call to configure it.",{"id":1930,"title":1931,"titles":1932,"content":1933,"level":199},"/docs/api/fields#fill-callback","Fill callback",[69,1926],"During the store and update requests, there are two steps before the value from the Request is attached to the model attribute. First, it is retrieved from the application request and passed to the fillCallback. Then, the value is passed through the storeCallback or updateCallback: You may intercept each of these with closures. Let's start with the fillCallback. It accepts a callable (an invokable class) or a Closure. The callable will receive the Request, the repository model (an empty one for storing and filled one for updating) and the attribute name: field('title')->fillCallback(function (RestifyRequest $request, Post $model, $attribute) {\n    $model->title = strtoupper($request->input('title_from_the_request'));\n}) This way you can get anything from the $request and perform any transformations with the value before storing.",{"id":1935,"title":1936,"titles":1937,"content":1938,"level":199},"/docs/api/fields#store-callback","Store callback",[69,1926],"Another handy interceptor is the storeCallback. This is the step that comes immediately before attaching the value from the\nrequest to the model attribute: This interceptor may be useful for modifying the value passed through the $request. Field::new('password')->storeCallback(function (RestifyRequest $request) {\n    return Hash::make($request->input('password'));\n});",{"id":1940,"title":1941,"titles":1942,"content":1943,"level":199},"/docs/api/fields#update-callback","Update callback",[69,1926],"The updateCallback works in the same manner. Let's use an invokable this time: Field::new('password')->updateCallback(new PasswordUpdateInvokable); Where the PasswordUpdateInvokable could be an invokable class: class PasswordUpdateInvokable \n{\n    public function __invoke(Request $request)\n    {\n        return Hash::make($request->input('password'));\n    }\n}",{"id":1945,"title":1946,"titles":1947,"content":1948,"level":199},"/docs/api/fields#store-bulk-callback","Store bulk callback",[69,1926],"For bulk store operations, you can use the storeBulkCallback to modify values during bulk creation: Field::new('slug')->storeBulkCallback(function (RestifyRequest $request) {\n    return Str::slug($request->input('title'));\n});",{"id":1950,"title":1951,"titles":1952,"content":1953,"level":199},"/docs/api/fields#index-callback","Index Callback",[69,1926],"Sometimes, you might want to transform an attribute from the database right before it is returned to the frontend. Transform the value for the following index request: Field::new('password')->indexCallback(function ($value) {\n    return Hash::make($value);\n});",{"id":1955,"title":1956,"titles":1957,"content":1958,"level":199},"/docs/api/fields#show-callback","Show callback",[69,1926],"Transform the value for the following show request: Field::new('password')->showCallback(function ($value) {\n    return Hash::make($value);\n});",{"id":1960,"title":1961,"titles":1962,"content":1963,"level":199},"/docs/api/fields#resolve-callback","Resolve callback",[69,1926],"Transform the value for both show and index requests: Field::new('password')->resolveCallback(function ($value) {\n    return Hash::make($value);\n});",{"id":1965,"title":1966,"titles":1967,"content":1968,"level":199},"/docs/api/fields#fields-actionable","Fields actionable",[69,1926],"At times, storing attributes might require the stored model before saving it. For example, let's say the Post model uses the media library, and has the media relationship that is a list of Media files: // PostRepository\n\npublic function fields(RestifyRequest $request): array\n{\n    return [\n        field('title'),\n        \n        field('files', \n            fn () => $this->model()->media()->pluck('file_name')\n        )\n        ->action(new AttachPostFileRestifyAction),\n    ];\n} So we have a virtual files field (it's not an actual database column) that uses a computed field to display the list of Post's files names. The ->action() calls and accepts an instance of a class that extends Binaryk\\LaravelRestify\\Actions\\Action: class AttachPostFileRestifyAction extends Action\n{\n    public function handle(RestifyRequest $request, Post $post): void\n    {\n        $post->addMediaFromRequest('file')\n            ->toMediaCollection();\n    }\n} The action gets the $request and the current $post model. Let's say the frontend has to create a post with a file: const data = new FormData;\ndata.append('file', blobFile);\ndata.append('title', 'Post title');\n\naxios.post(`api/restify/posts`, data); We were able to create the post and attach a file using media library in a single request. Otherwise, it would have implied creating 2 separate requests (post creation and file attaching). Actionable fields handle store, put, bulk store and bulk update requests.",{"id":1970,"title":1971,"titles":1972,"content":224,"level":188},"/docs/api/fields#fallbacks","Fallbacks",[69],{"id":1974,"title":1975,"titles":1976,"content":1977,"level":199},"/docs/api/fields#default-stored-value","Default Stored Value",[69,1971],"Usually, there is necessary to store a field as Auth::id(). This field will be automatically populated by Restify if\nyou specify the value value for it: Field::new('user_id')->value(Auth::id()); or by using a closure: Field::new('user_id')->hidden()->value(function(RestifyRequest $request, $model, $attribute) {\n    return $request->user()->id;\n});",{"id":1979,"title":1980,"titles":1981,"content":1982,"level":199},"/docs/api/fields#default-displayed-value","Default Displayed Value",[69,1971],"If you have a field which has null value into the database, you might want to return a fallback default value for\nthe frontend: Field::new('description')->default('N/A'); Now, for the fields that don't have a description into the database, it will return N/A. The default value is ONLY used for the READ, not for WRITE requests.",{"id":1984,"title":1975,"titles":1985,"content":1986,"level":199},"/docs/api/fields#default-stored-value-1",[69,1971],"During any (update or store requests), this is called after the fill and store callbacks. You can pass a callable or a value, and it will be attached to the model if no value provided otherwise. Imagine it's like attributes in the model: field('currency')->defaultCallback('EUR'),",{"id":1988,"title":1989,"titles":1990,"content":224,"level":188},"/docs/api/fields#customizations","Customizations",[69],{"id":1992,"title":1993,"titles":1994,"content":1995,"level":199},"/docs/api/fields#field-label","Field label",[69,1989],"Field label, so you can replace a field attribute spelling when it is returned to the frontend: Field::new('created_at')->label('sent_at') If you want to populate this value from a frontend request, you can use the label as a payload key.",{"id":1997,"title":1998,"titles":1999,"content":2000,"level":199},"/docs/api/fields#hidden-field","Hidden field",[69,1989],"Field can be setup as hidden: Field::new('token')->hidden(); // this will not be visible However, you can populate the field value when the entity is stored by using value: Field::new('token')->value(Str::random(32))->hidden();",{"id":2002,"title":2003,"titles":2004,"content":2005,"level":199},"/docs/api/fields#mcp-visibility-control","MCP Visibility Control",[69,1989],"When using Laravel Restify with Model Context Protocol (MCP), you can control field visibility specifically for MCP requests using dedicated methods: // Hide field from MCP requests completely\nField::new('secret_key')->hideFromMcp()\n\n// Show field only in MCP requests (hide from regular API)\nField::new('mcp_metadata')->showOnIndex(false)->showOnShow(false)->showOnMcp(true)\n\n// Conditionally hide based on user permissions\nField::new('admin_notes')->hideFromMcp(function($request, $repository) {\n    return !$request->user()->isAdmin();\n})\n\n// Show field in MCP based on user role\nField::new('sensitive_data')->showOnMcp(function($request, $repository) {\n    return $request->user()->can('view-sensitive', $repository);\n})",{"id":2007,"title":2008,"titles":2009,"content":2010,"level":834},"/docs/api/fields#mcp-visibility-methods","MCP Visibility Methods",[69,1989,2003],"showOnMcp($callback = true) - Control whether the field should be visible in MCP requestshideFromMcp($callback = true) - Hide the field from MCP requests (inverse of showOnMcp) Both methods accept either a boolean value or a callback function that receives the request and repository as parameters. MCP visibility rules take precedence over regular `showOnIndex`/`showOnShow` rules when processing MCP requests. Fields are visible in MCP by default unless explicitly hidden.",{"id":2012,"title":354,"titles":2013,"content":2014,"level":834},"/docs/api/fields#how-it-works",[69,1989,2003],"The MCP visibility system automatically detects when a request is coming from an MCP tool and applies the appropriate visibility rules: Regular API requests use showOnIndex() and showOnShow() rulesMCP requests use showOnMcp() and hideFromMcp() rulesDefault behavior - fields are visible in MCP unless explicitly hidden This allows you to have different field visibility for your regular API consumers versus AI agents accessing your data through MCP tools.",{"id":2016,"title":2017,"titles":2018,"content":2019,"level":199},"/docs/api/fields#field-descriptions","Field Descriptions",[69,1989],"Fields can have custom descriptions that are used when generating schema documentation, particularly useful for MCP tools and API documentation: public function fields(RestifyRequest $request)\n{\n    return [\n        field('status')\n            ->description('The current status of the item')\n            ->rules(['required', 'string']),\n            \n        field('feedbackable_id')\n            ->description('This is the id of the employee.')\n            ->rules(['required', 'string', 'max:26']),\n            \n        field('priority')\n            ->description(function($generatedDescription, $field, $repository) {\n                return $generatedDescription . ' - Values range from 1 (low) to 5 (high)';\n            }),\n    ];\n} The description() method accepts either: String: A static description textClosure: A callback that receives the auto-generated description, field instance, and repository for dynamic modifications When using a closure, you can: Modify the automatically generated descriptionAdd context-specific informationAccess field and repository data for dynamic descriptions The description callback receives three parameters: $generatedDescription - The automatically generated description based on field type and validation rules$field - The field instance$repository - The repository context",{"id":2021,"title":2022,"titles":2023,"content":2024,"level":199},"/docs/api/fields#custom-tool-schema","Custom Tool Schema",[69,1989],"When using MCP, you can define custom schema definitions for individual fields using the toolSchema() method: Field::new('status')->toolSchema(function ($field, $request, $repository) {\n    return [\n        'type' => 'string',\n        'enum' => ['draft', 'published', 'archived'],\n        'description' => 'The publication status of the content'\n    ];\n});\n\nField::new('settings')->toolSchema(function ($field, $request, $repository) {\n    return [\n        'type' => 'object',\n        'properties' => [\n            'theme' => ['type' => 'string'],\n            'notifications' => ['type' => 'boolean']\n        ],\n        'description' => 'User configuration settings'\n    ];\n}); The toolSchema() callback receives: $field - The field instance$request - The current request$repository - The parent repository This allows you to provide detailed schema information that helps MCP tools understand the structure and constraints of your data fields.",{"id":2026,"title":2027,"titles":2028,"content":224,"level":188},"/docs/api/fields#hooks","Hooks",[69],{"id":2030,"title":2031,"titles":2032,"content":2033,"level":199},"/docs/api/fields#after-store","After store",[69,2027],"You can handle the after field store callback: Field::new('title')->afterStore(function($value) {\n    dump($value);\n})",{"id":2035,"title":2036,"titles":2037,"content":2038,"level":199},"/docs/api/fields#after-update","After update",[69,2027],"You can handle the after field is updated callback: Field::new('title')->afterUpdate(function($value, $oldValue) {\n    dump($value, $oldValue);\n})",{"id":2040,"title":2041,"titles":2042,"content":2043,"level":188},"/docs/api/fields#file-fields","File fields",[69],"To illustrate the behavior of Restify file upload fields, let's assume our application's users can upload \"avatar\nphotos\" to their account. Our users' database table will have an avatar column. This column will contain the path\nto the profile on disk, or, when using a cloud storage provider such as Amazon S3, the profile photo's path within its\nbucket.",{"id":2045,"title":2046,"titles":2047,"content":2048,"level":199},"/docs/api/fields#defining-the-field","Defining the field",[69,2041],"Next, let's attach the file field to our UserRepository. In this example, we will create the field and instruct it to\nstore the underlying file on the public disk. This disk name should correspond to a disk name in your filesystems\nconfiguration file: use Binaryk\\LaravelRestify\\Fields\\File;\n\npublic function fields(RestifyRequest $request)\n{\n    return [\n        File::make('avatar')->disk('public')\n    ];\n} You can use field('avatar')->file() instead of File::make('avatar') as well.",{"id":2050,"title":2051,"titles":2052,"content":2053,"level":199},"/docs/api/fields#how-files-are-stored","How Files Are Stored",[69,2041],"When a file is uploaded by using this field, Restify will use\nLaravel's Filesystem integration to store the file from the disk of your choice\nwith a randomly generated filename. Once the file is stored, Restify will store the relative path to the file in the\nfile field's underlying database column.",{"id":2055,"title":2056,"titles":2057,"content":2058,"level":199},"/docs/api/fields#url-input-support","URL Input Support",[69,2041],"File fields also accept URL strings as input, providing flexibility when working with remote files or existing URLs: // You can send either a file upload or a URL string\nPOST /api/restify/users\n{\n    \"name\": \"John Doe\",\n    \"avatar\": \"https://example.com/images/avatar.jpg\"\n}\n\n// Or upload a file traditionally\nPOST /api/restify/users\nContent-Type: multipart/form-data\nname: John Doe\navatar: [binary file data] When a valid URL is provided: The URL is stored directly in the database columnIf storeOriginalName() is configured, the filename from the URL is extracted and storedValidation rules are automatically adjusted to accept both files and URLs To illustrate the default behavior of the File field, let's take a look at an equivalent route that would store the\nfile in the same way: use Illuminate\\Http\\Request;\n\nRoute::post('/avatar', function (Request $request) {\n    $path = $request->avatar->store('/', 'public');\n\n    $request->user()->update([\n        'avatar' => $path,\n    ]);\n}); If you are using the public disk with the local driver, you should run the php artisan storage:link Artisan\ncommand to create a symbolic link from public/storage to storage/app/public. To learn more about file storage in\nLaravel, check out the Laravel file storage documentation.",{"id":2060,"title":2061,"titles":2062,"content":2063,"level":199},"/docs/api/fields#image","Image",[69,2041],"The Image field behaves exactly like the File field; however, it will instruct Restify to only accept mimetypes of\ntype image/* for it: Image::make('avatar')->storeAs('avatar.jpg')",{"id":2065,"title":2066,"titles":2067,"content":2068,"level":199},"/docs/api/fields#storing-metadata","Storing Metadata",[69,2041],"In addition to storing the path to the file within the storage system, you may also instruct Restify to store the\noriginal client filename and its size (in bytes). You may accomplish this using the storeOriginalName and storeSize\nmethods. Each of these methods accepts the name of the column that you would want to store the file's information in: Image::make('avatar')\n    ->storeOriginalName('avatar_original')\n    ->storeSize('avatar_size')\n    ->storeAs('avatar.jpg') The image above will store the file with the name avatar.jpg in the avatar column, the original file name\ninto avatar_original column and file size in bytes under avatar_size column (only if these columns are fillable on\nyour model). You can use field('avatar')->image() instead of Image::make('avatar') as well.",{"id":2070,"title":2071,"titles":2072,"content":2073,"level":199},"/docs/api/fields#pruning-deletion","Pruning & Deletion",[69,2041],"File fields are deletable by default, so check out the following field definition: File::make('avatar') You have a request to delete the avatar of the user with the id 1: DELETE: api/restify/users/1/field/avatar You can override this behavior by using the deletable method: File::make('Photo')->disk('public')->deletable(false) Now, the field will not be deletable anymore.",{"id":2075,"title":2076,"titles":2077,"content":2078,"level":199},"/docs/api/fields#customizing-file-storage","Customizing File Storage",[69,2041],"Previously we learned that, by default, Restify stores the file using the store method of\nthe Illuminate\\Http\\UploadedFile class. However, you may fully customize this behavior based on your application's\nneeds.",{"id":2080,"title":2081,"titles":2082,"content":2083,"level":834},"/docs/api/fields#customizing-the-name-path","Customizing The Name / Path",[69,2041,2076],"If you only need to customize the name or path of the stored file on disk, you may use the path and storeAs methods\nof the File field: use Illuminate\\Http\\Request;\n\nFile::make('avatar')\n    ->disk('s3')\n    ->path($request->user()->id.'-attachments')\n    ->storeAs(function (Request $request) {\n        return sha1($request->attachment->getClientOriginalName());\n    }),",{"id":2085,"title":2086,"titles":2087,"content":2088,"level":834},"/docs/api/fields#customizing-the-entire-storage-process","Customizing The Entire Storage Process",[69,2041,2076],"However, if you would like to take full control over the file storage logic of a field, you may use the store\nmethod. The store method accepts a callable which receives the incoming HTTP request and the model's instance associated\nwith the request: use Illuminate\\Http\\Request;\n\nFile::make('avatar')\n    ->store(function (Request $request, $model) {\n        return [\n            'attachment' => $request->attachment->store('/', 's3'),\n            'attachment_name' => $request->attachment->getClientOriginalName(),\n            'attachment_size' => $request->attachment->getSize(),\n        ];\n    }), As you can see in the example above, the store callback is returning an array of keys and values. These key / value\npairs are mapped onto your model's instance before it is saved to the database, allowing you to update one or many of the\nmodel's database columns after your file is stored.",{"id":2090,"title":2091,"titles":2092,"content":2093,"level":199},"/docs/api/fields#customizing-file-display","Customizing File Display",[69,2041],"By default, Restify will display the file's stored path name. However, you may customize this behavior.",{"id":2095,"title":2096,"titles":2097,"content":2098,"level":834},"/docs/api/fields#displaying-temporary-url","Displaying temporary url",[69,2041,2091],"For disks such as S3, you may instruct Restify to display a temporary URL to the file instead of the stored path name: field('path')\n      ->file()\n      ->path(\"documents/\".Auth::id())\n      ->resolveUsingTemporaryUrl()\n      ->disk('s3'), The resolveUsingTemporaryUrl accepts 3 arguments: $resolveTemporaryUrl - a boolean to determine if the temporary url should be resolved. Defaults to true.$expiration - A CarbonInterface to determine the time before the URL expires. Defaults to 5 minutes.$options - An array of options to pass to the temporaryUrl method of the Illuminate\\Contracts\\Filesystem\\Filesystem implementation. Defaults to an empty array.",{"id":2100,"title":2101,"titles":2102,"content":2103,"level":834},"/docs/api/fields#displaying-full-url","Displaying full url",[69,2041,2091],"For disks such as public, you may instruct Restify to display a full URL to the file instead of the stored path name: field('path')\n      ->file()\n      ->path(\"documents/\".Auth::id())\n      ->resolveUsingFullUrl()\n      ->disk('public'),",{"id":2105,"title":2106,"titles":2107,"content":2108,"level":834},"/docs/api/fields#storeables","Storeables",[69,2041,2091],"Of course, performing all of your file storage logic within a Closure can cause your resource to become bloated. For\nthat reason, Restify allows you to pass an \"Storable\" class to the store method: File::make('avatar')->store(AvatarStore::class), The storable class should be a simple PHP class that implements the Binaryk\\LaravelRestify\\Repositories\\Storable contract: \u003C?php\n\nnamespace Binaryk\\LaravelRestify\\Tests\\Fixtures\\User;\n\nuse Binaryk\\LaravelRestify\\Repositories\\Storable;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Http\\Request;\n\nclass AvatarStore implements Storable\n{\n    public function handle(Request $request, Model $model, $attribute): array\n    {\n        return [\n            'avatar' => $request->file('avatar')->storeAs('/', 'avatar.jpg', 'customDisk')\n        ];\n    }\n}",{"id":2110,"title":2111,"titles":2112,"content":2113,"level":199},"/docs/api/fields#command","Command",[69,2041],"You can use the php artisan restify:store AvatarStore command to generate a store file.",{"id":2115,"title":2116,"titles":2117,"content":2118,"level":188},"/docs/api/fields#lazy-loading","Lazy Loading",[69],"Fields can be configured to lazy load relationships, which is particularly useful for computed attributes that depend on related models. This helps avoid N+1 queries by ensuring relationships are loaded only when needed.",{"id":2120,"title":2121,"titles":2122,"content":2123,"level":199},"/docs/api/fields#making-fields-lazy","Making Fields Lazy",[69,2116],"Use the lazy() method to mark a field for lazy loading: public function fields(RestifyRequest $request)\n{\n    return [\n        // Lazy load the 'tags' relationship when displaying profileTagNames\n        field('profileTagNames', fn() => $this->model()->profileTagNames)\n            ->lazy('tags'),\n            \n        // Lazy load using the field's attribute name (if it matches the relationship)\n        field('tags', fn() => $this->model()->tags->pluck('name'))\n            ->lazy(),\n            \n        // Another example with user relationship\n        field('authorName', fn() => $this->model()->user->name ?? 'Unknown')\n            ->lazy('user'),\n    ];\n}",{"id":2125,"title":354,"titles":2126,"content":2127,"level":199},"/docs/api/fields#how-it-works-1",[69,2116],"When you have a model attribute like this: class Post extends Model\n{\n    public function getProfileTagNamesAttribute(): array\n    {\n        return $this->tags()->pluck('name')->toArray();\n    }\n    \n    public function tags()\n    {\n        return $this->belongsToMany(Tag::class);\n    }\n} You can create a field that efficiently loads this data: field('profileTagNames', fn() => $this->model()->profileTagNames)\n    ->lazy('tags') This ensures that: The tags relationship is loaded before the field value is computedMultiple fields using the same relationship won't cause additional queriesThe computed value can safely access the relationship data",{"id":2129,"title":2130,"titles":2131,"content":2132,"level":199},"/docs/api/fields#lazy-loading-methods","Lazy Loading Methods",[69,2116],"The CanLoadLazyRelationship trait provides the following methods: lazy(?string $relationshipName = null) - Mark the field as lazy and optionally specify the relationship nameisLazy(RestifyRequest $request) - Check if the field is configured for lazy loadinggetLazyRelationshipName() - Get the name of the relationship to lazy load",{"id":2134,"title":404,"titles":2135,"content":2136,"level":199},"/docs/api/fields#benefits",[69,2116],"Performance: Prevents N+1 queries when dealing with computed attributesEfficiency: Relationships are loaded only once, even if multiple fields depend on themFlexibility: Works with any relationship type (BelongsTo, HasMany, ManyToMany, etc.)Clean Code: Keeps your field definitions simple while ensuring optimal database usage",{"id":2138,"title":2139,"titles":2140,"content":224,"level":188},"/docs/api/fields#utility-methods","Utility Methods",[69],{"id":2142,"title":2143,"titles":2144,"content":2145,"level":199},"/docs/api/fields#repository-management","Repository Management",[69,2139],"Fields can be assigned to repositories programmatically: $field = Field::new('title');\n$field->setRepository($repository);\n$field->setParentRepository($parentRepository); These methods are primarily used internally by Restify but can be useful when building custom field logic.",{"id":2147,"title":2148,"titles":2149,"content":224,"level":199},"/docs/api/fields#legacy-methods","Legacy Methods",[69,2139],{"id":2151,"title":2152,"titles":2153,"content":2154,"level":834},"/docs/api/fields#deprecated-append-method","Deprecated append() Method",[69,2139,2148],"The append() method has been deprecated in favor of value(). Use value() instead:// Deprecated\nfield('user_id')->append(Auth::id());\n\n// Recommended  \nfield('user_id')->value(Auth::id()); html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":74,"title":73,"titles":2156,"content":2157,"level":182},[],"Learn how Restify handles Eloquent relationships. Define HasMany, BelongsTo, and other eager fields in the related() method to expose nested API resources.",{"id":2159,"title":920,"titles":2160,"content":2161,"level":188},"/docs/api/relations#introduction",[73],"Eloquent provides a large variety of relationships. You can read about them here.\nRestify handles all relationships and gives you an expressive way to list resource relationships.",{"id":2163,"title":2164,"titles":2165,"content":2166,"level":188},"/docs/api/relations#definition","Definition",[73],"The list of relationships should be defined into a repository method called related: public static function related(): array\n{\n    return [];\n}",{"id":2168,"title":2169,"titles":2170,"content":2171,"level":199},"/docs/api/relations#eager-fields","Eager fields",[73,2164],"The related method will return an array that should be a key-value pair, where the key is the related name that the API will request, and the value could be an instance of Binaryk\\LaravelRestify\\Fields\\EagerField or a relationship name defined in your model. Each EagerField declaration is similar to the Field one. The first argument is the model relationship name. The second argument is a repository that represents the related entity. Let's say we have a User that has a list of posts. We will define it this way: HasMany::make('posts', PostRepository::class), or: HasMany::make('posts'), Restify 7+ will guess the serialization repository using the key, so you don't necessarily have to specify it:",{"id":2173,"title":2174,"titles":2175,"content":2176,"level":199},"/docs/api/relations#related-declaration","Related Declaration",[73,2164],"Let's see how can we inform a repository about its relationships: // CompanyRepository\npublic static function related(): array\n{\n    return [\n        'usersRelationship' => HasMany::make('users', UserRepository::class),\n        \n        HasMany::make('posts'),\n        \n        'extraData' => fn() => ['location' => 'Romania'],\n        \n        'extraMeta' => new Invokable(),\n        \n        'country',\n    ];\n} Above we can see a few types of relationships declarations that Restify provides. Let's explain them.",{"id":2178,"title":2179,"titles":2180,"content":2181,"level":834},"/docs/api/relations#long-definition","Long definition",[73,2164,2174],"'usersRelationship' => HasMany::make('users', UserRepository::class), This means that there is a relationship of the hasMany type declared in the Company model. The Eloquent relationship name is users (see the first argument of the HasMany field): public function users(): \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n{\n    return $this->hasMany(User::class);\n} The key usersRelationship represents the query param the API exposes to load the list of users: GET: api/companies?related=usersRelationship The UserRepository represents the repository class that serializes the users list.",{"id":2183,"title":2184,"titles":2185,"content":2186,"level":834},"/docs/api/relations#short-definition","Short definition",[73,2164,2174],"HasMany::make('posts'), Usually the key (query param) and the actual Eloquent relationship names are the same, so Restify provides a shorter version of defining the relationship. In this case the name of the query param will be the same as the relationship name - posts. The name of the repository PostRepository will be resolved based on the same key and $uriKey of the repository. The request will look like this: GET: api/companies?related=posts",{"id":2188,"title":2189,"titles":2190,"content":2191,"level":834},"/docs/api/relations#callables","Callables",[73,2164,2174],"'extraData' => fn() => ['location' => 'Romania'],\n\n'extraMeta' => new Invokable() Restify allows you to resolve specific data using callable functions or invokable (classes with a single public __invoke method). You can return any kind of data from these callables. It'll be serialized accordingly. The query param in this case should match the key: GET: api/companies?related=extraData,extraMeta",{"id":2193,"title":2194,"titles":2195,"content":2196,"level":834},"/docs/api/relations#forwarding","Forwarding",[73,2164,2174],"'country', If you simply define a key in the related, Restify will forward your request to the associated model. Your model could return anything, as it might be an Eloquent relationship or any primary data. Let's take a look over all the relationships that Restify provides:",{"id":2198,"title":2199,"titles":2200,"content":2201,"level":199},"/docs/api/relations#frontend-request","Frontend request",[73,2164],"In order to get the related resources, you need to send a GET request to: GET `/api/restify/users?include=posts` Sometimes, you might want to load specific columns from the database into the response. For example, if you have a Post model with an id, title, and a description column, you might want to load only the title and the description column in the response. In order to do this, you can use the following request: GET /users/1?include=posts[title|description]",{"id":2203,"title":2204,"titles":2205,"content":2206,"level":199},"/docs/api/relations#nested-relationships","Nested relationships",[73,2164],"Let's assume you have the CompanyRepository: // CompanyRepository\npublic static function related(): array\n{\n    return [\n        HasMany::make('users'),\n    ];\n} In the UserRepository you have a relationship to a list of user posts and roles: // UserRepository\npublic static function related(): array\n{\n    return [\n        HasMany::make('posts'),\n        MorphToMany::make('roles'),\n    ];\n} In PostRepository you might have a list of comments for each post: // PostRepository\npublic static function related(): array\n{\n    return [\n        HasMany::make('comments'),\n    ];\n} In order to get the company's users with their posts and roles, you can follow the laravel syntax for eager loading into the request query: GET: /api/restify/companies?include=users.posts,users.roles This request will return a list like this: {\n  \"data\": {\n    \"id\": \"91c2bdd0-bf6f-4717-b1c4-a6131843ba56\",\n    \"type\": \"companies\",\n    \"attributes\": {\n      \"name\": \"Binar Code\"\n    },\n    \"relationships\": {\n      \"users\": [{\n        \"id\": \"3\",\n        \"type\": \"users\",\n        \"attributes\": {\n          \"name\": \"Eduard\"\n        },\n        \"relationships\": {\n          \"posts\": [{\n            \"id\": \"1\",\n            \"type\": \"posts\",\n            \"attributes\": {\n              \"title\": \"Post title\"\n            }\n          }],\n          \"roles\": [{\n            \"id\": \"1\",\n            \"type\": \"roles\",\n            \"attributes\": {\n              \"name\": \"admin\"\n            }\n          }]\n        }\n      }]\n    }\n  }\n} You can also specify and load the comments of the posts: GET: /api/restify/companies?include=users.posts.comments,users.roles Or specify the exact columns that you want to load for each nested layer: GET: /api/restify/companies?include=users[name].posts[id|title].comments[comment],users.roles[name] Getting specific columns will make your requests more performant.",{"id":2208,"title":2209,"titles":2210,"content":2211,"level":199},"/docs/api/relations#meta-information","Meta information",[73,2164],"Starting with Restify 7+, meta information for related (in index requests) will not be displayed. For more details read the repository meta.",{"id":2213,"title":2214,"titles":2215,"content":2216,"level":188},"/docs/api/relations#belongsto-morphone","BelongsTo & MorphOne",[73],"The BelongsTo and MorphOne eager fields work in a similar way, so let's take the BelongsTo as an example. Let's assume each Post belongsTo a User. To return the post's owner, we will define it like this: // PostRepository\npublic static function related(): array\n{\n    return [\n        'owner' => \\Binaryk\\LaravelRestify\\Fields\\BelongsTo::make('user', UserRepository::class),\n    ];\n} The model should define the relationship user: public function user()\n{\n    return $this->belongsTo(User::class);\n} Now the frontend can list a post or posts including the following relationship: GET: api/restify/posts/1?include=owner {\n  \"data\": {\n    \"id\": \"91c2bdd0-bf6f-4717-b1c4-a6131843ba56\",\n    \"type\": \"posts\",\n    \"attributes\": {\n      \"title\": \"Culpa qui accusamus eaque sint.\",\n      \"description\": \"Id illo et quidem nobis reiciendis molestiae.\"\n    },\n    \"relationships\": {\n      \"owner\": {\n        \"id\": \"3\",\n        \"type\": \"users\",\n        \"attributes\": {\n          \"name\": \"Laborum vel esse dolorem amet consequatur.\",\n          \"email\": \"jacobi.ferne@gmail.com\"\n        },\n        \"meta\": {\n          \"authorizedToShow\": true,\n          \"authorizedToStore\": true,\n          \"authorizedToUpdate\": false,\n          \"authorizedToDelete\": false\n        }\n      }\n    },\n    \"meta\": {\n      \"authorizedToShow\": true,\n      \"authorizedToStore\": true,\n      \"authorizedToUpdate\": true,\n      \"authorizedToDelete\": true\n    }\n  }\n}",{"id":2218,"title":2219,"titles":2220,"content":2221,"level":199},"/docs/api/relations#searchable-belongs-to","Searchable belongs to",[73,2214],"The BelongsTo field allows you to use the search endpoint to search over a column from the belongsTo relationship by simply using the searchables call: BelongsTo::make('user')->searchable('name', 'email') The searchable method accepts multiple database attributes from the related entity (users in our case). Therefore, if we get the following search request, it'll also search into the related user's name and email: GET: api/restify/companies?related=user&search=\"John\" You can check if a relation is searchable using: $field = BelongsTo::make('user')->searchable('name');\n$isSearchable = $field->isSearchable(); // true\n$attributes = $field->getSearchables(); // ['name']",{"id":2223,"title":2224,"titles":2225,"content":2226,"level":834},"/docs/api/relations#custom-search-callbacks","Custom Search Callbacks",[73,2214,2219],"For advanced search scenarios, you can provide a custom callback to completely control the search behavior: BelongsTo::make('user')->searchable(function ($query, $request, $value, $field, $repository) {\n    return $query->whereHas('user', function ($q) use ($value) {\n        $q->where('name', 'ilike', \"%{$value}%\")\n          ->orWhere('email', 'ilike', \"%{$value}%\")\n          ->orWhere('phone', 'like', \"%{$value}%\");\n    });\n}) The callback receives the following parameters: $query - The main query builder instance$request - The current RestifyRequest instance$value - The search value from the request$field - The BelongsTo field instance$repository - The current repository instance This approach provides maximum flexibility for complex search requirements while maintaining the same API interface.",{"id":2228,"title":2229,"titles":2230,"content":2231,"level":188},"/docs/api/relations#hasone","HasOne",[73],"The HasOne field corresponds to a hasOne Eloquent relationship. For example, let's assume a User model hasOne Phone model. We may add the relationship to our UserRepository like so: // UserRepository\npublic static function related(): array\n{\n  return [\n      \\Binaryk\\LaravelRestify\\Fields\\HasOne::make('phone', PhoneRepository::class),\n  ];\n}",{"id":2233,"title":2234,"titles":2235,"content":2236,"level":199},"/docs/api/relations#sortable-hasone-relations","Sortable HasOne Relations",[73,2229],"HasOne relations can be made sortable: HasOne::make('phone')->sortable('number') This allows sorting by the related model's attributes: GET: api/restify/users?sort=phone.number The json response structure will be the same as previously: {\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"users\",\n    \"attributes\": {\n      \"name\": \"Et maxime voluptatem cumque accusamus sit.\"\n    },\n    \"relationships\": {\n      \"phone\": {\n        \"id\": \"2\",\n        \"type\": \"phones\",\n        \"attributes\": {\n          \"phone\": \"+40 766 444 22\"\n        },\n        \"meta\": {\n          \"authorizedToShow\": false,\n          \"authorizedToStore\": true,\n          \"authorizedToUpdate\": false,\n          \"authorizedToDelete\": false\n        }\n      },\n      ...",{"id":2238,"title":2239,"titles":2240,"content":2241,"level":188},"/docs/api/relations#hasmany-morphmany","HasMany & MorphMany",[73],"The HasMany and MorphMany fields correspond to a hasMany and morphMany Eloquent relationship. For example, let's assume a User\nmodel hasMany Post models. We may add the relationship to our UserRepository as shown: // UserRepository\npublic static function related(): array\n{\n  return [\n      \\Binaryk\\LaravelRestify\\Fields\\HasMany::make('posts', PostRepository::class),\n  ];\n} In addition, you will get back the posts relationship: {\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"users\",\n    \"attributes\": {\n      \"name\": \"Et maxime voluptatem cumque accusamus sit.\"\n    },\n    \"relationships\": {\n      \"posts\": [\n        {\n          \"id\": \"91c2bdd0-ccf6-49ec-9ae9-8bae1d39c100\",\n          \"type\": \"posts\",\n          \"attributes\": {\n            \"title\": \"Rem suscipit tempora ullam accusantium in rerum.\",\n            \"description\": \"Vero nostrum quasi velit molestiae animi neque.\"\n          },\n          \"meta\": {\n            \"authorizedToShow\": true,\n            \"authorizedToStore\": true,\n            \"authorizedToUpdate\": true,\n            \"authorizedToDelete\": true\n          }\n        }\n      ]\n    },\n    \"meta\": {\n      \"authorizedToShow\": true,\n      \"authorizedToStore\": true,\n      \"authorizedToUpdate\": false,\n      \"authorizedToDelete\": false\n    }\n  }\n}",{"id":2243,"title":2244,"titles":2245,"content":2246,"level":199},"/docs/api/relations#paginate","Paginate",[73,2239],"HasMany field returns 15 entries in the relationships. This could be customizable from the repository (the\nrepository being in this case the class of the related resource) class by using: public static int $defaultRelatablePerPage = 100;",{"id":2248,"title":2249,"titles":2250,"content":2251,"level":199},"/docs/api/relations#relatable-per-page","Relatable per page",[73,2239],"You can also use the query ?relatablePerPage=100. GET: api/restify/users?related=posts&relatablePerPage=100 When using relatablePerPage query param, it will paginate all the relatable entities with that size.",{"id":2253,"title":2254,"titles":2255,"content":2256,"level":188},"/docs/api/relations#belongstomany-morphtomany","BelongsToMany & MorphToMany",[73],"The BelongsToMany and MorphToMany field corresponds to a belongsToMany or morphToMany Eloquent relationship. For example, let's assume a User\nmodel belongsToMany Role models. We may add the relationship to our UserRepository like this: // CompanyRepository\npublic static function related(): array\n{\n  return [\n      \\Binaryk\\LaravelRestify\\Fields\\BelongsToMany::make('users', UserRepository::class),\n  ];\n}",{"id":2258,"title":2259,"titles":2260,"content":2261,"level":199},"/docs/api/relations#pivot-fields","Pivot fields",[73,2254],"If your belongsToMany relationship interacts with additional \"pivot\" attributes that are stored on the intermediate\ntable of the many-to-many relationship, you may also attach those to your BelongsToMany Restify Field. Once these\nfields are attached to the relationship field and the relationship has been defined on both sides, they will be\ndisplayed on the request. For example, let's assume our User model belongsToMany Role models. On our user_role intermediate table, let's\nimagine we have a policy field that contains a simple text about the relationship. We can attach this pivot field\nto the BelongsToMany field by using the fields method: BelongsToMany::make('users', RoleRepository::class)->withPivot(\n    field('is_admin')\n), You might also need to define this in the User model: public function users()\n{\n   return $this->belongsToMany(User::class, 'user_company')->withPivot('is_admin');\n} Now, let's try to get the list of companies with users: GET: /api/restify/company/1?include=users {\n  \"data\": {\n    \"id\": \"1\",\n    \"type\": \"companies\",\n    \"attributes\": {\n      \"name\": \"ut\"\n    },\n    \"relationships\": {\n      \"users\": [\n        {\n          \"id\": \"1\",\n          \"type\": \"users\",\n          \"attributes\": {\n            \"name\": \"Linnea Rowe Sr.\",\n            \"email\": \"tledner@example.com\",\n          },\n          \"meta\": {\n            \"authorizedToShow\": true,\n            \"authorizedToStore\": true,\n            \"authorizedToUpdate\": true,\n            \"authorizedToDelete\": true\n          },\n          \"pivots\": {\n            \"is_admin\": true\n          }\n        }\n      ]\n    },\n    \"meta\": {\n      \"authorizedToShow\": true,\n      \"authorizedToStore\": true,\n      \"authorizedToUpdate\": true,\n      \"authorizedToDelete\": true\n    }\n  }\n}",{"id":2263,"title":2264,"titles":2265,"content":2266,"level":199},"/docs/api/relations#attach-related","Attach related",[73,2254],"Once you have defined the BelongsToMany field, you can now attach User to a Company just like this: POST: api/restify/companies/1/attach/users Payload: {\n  \"users\": [1, 2],\n  \"is_admin\": true\n}",{"id":2268,"title":2269,"titles":2270,"content":2271,"level":199},"/docs/api/relations#authorize-attach","Authorize attach",[73,2254],"You have a few options to authorize the attach endpoint. First, you can define the policy method attachUsers. The name should start with attach and suffix with the CamelCase name of the model's relationship name: public function attachUsers(User $authenticatedUser, Company $company, User $userToBeAttached): bool\n{ \n    return $authenticatedUser->isAdmin();\n} The policy attachUsers method will be called for each individual userToBeAttached. However, if you attach - 1, 3 ids, this method will be called twice. Another way to authorize this is by using the canAttach method to the Eager field directly. This method accepts an invokable class instance or a closure: 'users' => BelongsToMany::make('users',  UserRepository::class)\n            ->canAttach(function ($request, $pivot) {\n                return $request->user()->isAdmin();\n            }),",{"id":2273,"title":2274,"titles":2275,"content":2276,"level":199},"/docs/api/relations#override-attach","Override attach",[73,2254],"You are free to intercept the attach operation entirely and override it by using a closure or an invokable: 'users' => BelongsToMany::make('users',  UserRepository::class)\n            ->attachCallback(function ($request, $repository, $company) {\n                $company->users()->attach($request->input('users'));\n            }), Or using an invokable : 'users' => BelongsToMany::make('users',  UserRepository::class)\n            ->attachCallback(new AttachCompanyUsers), and then define the class: use Illuminate\\Http\\Request;\n\nclass AttachCompanyUsers\n{\n    public function __invoke(Request $request, CompanyRepository $repository, Company $company): void\n    {\n        $company->users()->attach($request->input('users'));\n    }\n}",{"id":2278,"title":2279,"titles":2280,"content":2281,"level":199},"/docs/api/relations#sync-related","Sync related",[73,2254],"You can also sync your BelongsToMany field. Say you have to sync permissions to a role. You can do it like this: POST: api/restify/roles/1/sync/permissions Payload: {\n  \"permissions\": [1, 2]\n} Under the hood this will call the sync method on the BelongsToMany relationship: // $role of the id 1\n\n$role->permissions()->sync($request->input('permissions'));",{"id":2283,"title":2284,"titles":2285,"content":2286,"level":199},"/docs/api/relations#authorize-sync","Authorize sync",[73,2254],"You can define a policy method syncPermissions. The name should start with sync and suffix with the plural CamelCase name of the model's relationship name: public function syncPermissions(User $authenticatedUser, Company $company, Collection $keys): bool\n{ \n    // $keys are the primary keys of the related model (permissions in our case) Restify is trying to `sync`\n}",{"id":2288,"title":2289,"titles":2290,"content":2291,"level":199},"/docs/api/relations#detach-related","Detach related",[73,2254],"As soon we declared the BelongsToMany relationship, Restify automatically registers the detach endpoint: POST: api/restify/companies/1/detach/users Using the payload: {\n  \"users\": [1]\n}",{"id":2293,"title":2294,"titles":2295,"content":2296,"level":199},"/docs/api/relations#authorize-detach","Authorize detach",[73,2254],"You have a few options to authorize the detach endpoint. Primarily, you can define the policy method detachUsers, as the name should start with detach and suffix with the CamelCase name of the model relationship name: public function detachUsers(User $authenticatedUser, Company $company, User $userToBeDetached): bool\n{ \n    return $authenticatedUser->isAdmin();\n} The policy detachUsers method will be called for each individual userToBeDetached. If you detach - 1, 3 ids, this method will be called twice. Another way to authorize this is by using the canDetach method to the Eager field directly. This method accepts an invokable class instance or a closure: 'users' => BelongsToMany::make('users',  UserRepository::class)\n            ->canDetach(\n                fn($request, $pivot) => $request->user()->can('detach', $pivot)\n            ),",{"id":2298,"title":2299,"titles":2300,"content":2301,"level":199},"/docs/api/relations#override-detach","Override detach",[73,2254],"You are free to intercept the detach method entirely and override it by using a closure or an invokable: 'users' => BelongsToMany::make('users',  UserRepository::class)\n            ->detachCallback(function ($request, $repository, $company) {\n                $company->users()->detach($request->input('users'));\n            }), Or using an invokable : 'users' => BelongsToMany::make('users',  UserRepository::class)\n            ->detachCallback(new DetachCompanyUsers), and then define the class: use Illuminate\\Http\\Request;\n\nclass DetachCompanyUsers\n{\n    public function __invoke(Request $request, CompanyRepository $repository, Company $company): void\n    {\n        $company->users()->detach($request->input('users'));\n    }\n}\n\n## Validation for Attach Operations\n\nYou can add custom validation for attach operations:\n\n```php\nBelongsToMany::make('users', UserRepository::class)\n    ->validationCallback(function ($request, $pivot) {\n        return [\n            'users.*' => 'exists:users,id',\n            'is_admin' => 'boolean'\n        ];\n    })\n    ->unique() // Prevents duplicate attachments The validationCallback receives the request and pivot data, and should return validation rules. The unique() method prevents duplicate attachments automatically.",{"id":2303,"title":2304,"titles":2305,"content":2306,"level":188},"/docs/api/relations#column-selection-in-relations","Column Selection in Relations",[73],"You can specify which columns to load for relations using Laravel's column selection syntax:",{"id":2308,"title":2309,"titles":2310,"content":2311,"level":199},"/docs/api/relations#basic-column-selection","Basic Column Selection",[73,2304],"GET: /api/restify/users?include=posts[id,title,created_at]",{"id":2313,"title":2314,"titles":2315,"content":2316,"level":199},"/docs/api/relations#nested-column-selection","Nested Column Selection",[73,2304],"GET: /api/restify/companies?include=users[id,name].posts[title].comments[comment]",{"id":2318,"title":2319,"titles":2320,"content":2321,"level":199},"/docs/api/relations#mixed-column-selection","Mixed Column Selection",[73,2304],"// In your repository\nHasMany::make('posts')->columns(['id', 'title', 'published_at'])",{"id":2323,"title":2324,"titles":2325,"content":224,"level":188},"/docs/api/relations#advanced-sorting","Advanced Sorting",[73],{"id":2327,"title":2328,"titles":2329,"content":2330,"level":199},"/docs/api/relations#sorting-by-related-fields","Sorting by Related Fields",[73,2324],"Both BelongsTo and HasOne relations support sorting: // BelongsTo sorting\nBelongsTo::make('user')->sortable('name')\n\n// HasOne sorting  \nHasOne::make('profile')->sortable('bio')",{"id":2332,"title":2333,"titles":2334,"content":2335,"level":199},"/docs/api/relations#json-attribute-sorting","JSON Attribute Sorting",[73,2324],"Relations can sort by JSON attributes: BelongsTo::make('user')->sortable('preferences->theme')",{"id":2337,"title":2338,"titles":2339,"content":2340,"level":199},"/docs/api/relations#custom-sort-logic","Custom Sort Logic",[73,2324],"You can define custom sorting logic: use Binaryk\\\\LaravelRestify\\\\Filters\\\\SortableFilter;\n\nSortableFilter::make()\n    ->usingClosure(function ($query, $direction) {\n        return $query->orderBy('custom_logic', $direction);\n    })",{"id":2342,"title":2343,"titles":2344,"content":2345,"level":188},"/docs/api/relations#morph-relations","Morph Relations",[73],"Laravel Restify supports all morph relationship types:",{"id":2347,"title":2348,"titles":2349,"content":2350,"level":199},"/docs/api/relations#morphone","MorphOne",[73,2343],"// CommentRepository\npublic static function related(): array\n{\n    return [\n        MorphOne::make('commentable', PostRepository::class),\n    ];\n}",{"id":2352,"title":2353,"titles":2354,"content":2355,"level":199},"/docs/api/relations#morphmany","MorphMany",[73,2343],"// PostRepository  \npublic static function related(): array\n{\n    return [\n        MorphMany::make('comments', CommentRepository::class),\n    ];\n}",{"id":2357,"title":2358,"titles":2359,"content":2360,"level":199},"/docs/api/relations#morphtomany","MorphToMany",[73,2343],"// PostRepository\npublic static function related(): array\n{\n    return [\n        MorphToMany::make('tags', TagRepository::class)->withPivot('created_at'),\n    ];\n}",{"id":2362,"title":1250,"titles":2363,"content":224,"level":188},"/docs/api/relations#relationship-authorization",[73],{"id":2365,"title":2366,"titles":2367,"content":2368,"level":199},"/docs/api/relations#repository-level-authorization","Repository-Level Authorization",[73,1250],"Relations inherit authorization from their target repositories. You can customize this: HasMany::make('posts')->canEnableRelationship(function ($request) {\n    return $request->user()->can('view-posts');\n})",{"id":2370,"title":2371,"titles":2372,"content":2373,"level":199},"/docs/api/relations#policy-based-authorization","Policy-Based Authorization",[73,1250],"Define policy methods for relation operations: // In your Policy class\npublic function viewPosts(User $user, Company $company): bool\n{\n    return $user->can('view', $company);\n}\n\npublic function attachUsers(User $user, Company $company, User $userToAttach): bool  \n{\n    return $user->isAdmin();\n}\n\npublic function detachUsers(User $user, Company $company, User $userToDetach): bool\n{\n    return $user->isAdmin(); \n}\n\npublic function syncPermissions(User $user, Role $role, Collection $permissionIds): bool\n{\n    return $user->can('manage-permissions');\n}",{"id":2375,"title":2376,"titles":2377,"content":224,"level":188},"/docs/api/relations#performance-optimizations","Performance Optimizations",[73],{"id":2379,"title":2380,"titles":2381,"content":2382,"level":199},"/docs/api/relations#eager-loading-prevention","Eager Loading Prevention",[73,2376],"Relations automatically prevent circular references and deep nesting to avoid performance issues.",{"id":2384,"title":2385,"titles":2386,"content":2387,"level":199},"/docs/api/relations#pagination-control","Pagination Control",[73,2376],"Control relation pagination globally: // In your Repository\npublic static int $defaultRelatablePerPage = 50; Or per request: GET: /api/restify/users?include=posts&relatablePerPage=25",{"id":2389,"title":2390,"titles":2391,"content":2392,"level":199},"/docs/api/relations#selective-column-loading","Selective Column Loading",[73,2376],"Always specify only needed columns: GET: /api/restify/users?include=posts[id,title]&fields[users]=id,name",{"id":2394,"title":2395,"titles":2396,"content":224,"level":188},"/docs/api/relations#debugging-relations","Debugging Relations",[73],{"id":2398,"title":2399,"titles":2400,"content":2401,"level":199},"/docs/api/relations#relation-state","Relation State",[73,2395],"Check relation loading state: $related = Related::make('posts', $field);\n$isEager = $related->isEager(); // boolean\n$relation = $related->getRelation(); // string",{"id":2403,"title":2404,"titles":2405,"content":2406,"level":199},"/docs/api/relations#query-analysis","Query Analysis",[73,2395],"Relations support query state tracking: $relatedQuery = RelatedQuery::fromToken('posts[id,title]');\n$columns = $relatedQuery->columns(); // ['id', 'title']  \n$isSerialized = $relatedQuery->isSerialized(); // boolean html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .su27w, html code.shiki .su27w{--shiki-light:#916B53;--shiki-default:#916B53;--shiki-dark:#916B53}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":78,"title":77,"titles":2408,"content":2409,"level":182},[],"Learn how Laravel Restify's RestController and RestResponse structure help you return consistent, JSON:API-compatible responses across all your API endpoints.",{"id":2411,"title":920,"titles":2412,"content":2413,"level":188},"/docs/api/rest-methods#introduction",[77],"The API response format must stay consistent throughout the application. Ideally, it would be good to follow a standard such as\nthe JSON:API, so your frontend app could align with the API nicely. Restify provides several different approaches to respond consistently to the application's incoming request. By default,\nRestify's base rest controller class uses a RestResponse structure which provides a convenient method to respond to\nthe HTTP request with a variety of handy magical methods.",{"id":2415,"title":2416,"titles":2417,"content":2418,"level":188},"/docs/api/rest-methods#restify-response-quickstart","Restify Response Quickstart",[77],"To learn about Restify's helpful response, let's take a look at a complete example of responding to a request and returning the data\nback to the client.",{"id":2420,"title":2421,"titles":2422,"content":2423,"level":199},"/docs/api/rest-methods#defining-the-route","Defining The Route",[77,2416],"First, let's assume we have the following routes defined in our routes/api.php file: Route::post('users', 'UserController@store');\n\nRoute::get('users/{id}', 'UserController@show'); The GET route will return back a user for the given id.",{"id":2425,"title":2426,"titles":2427,"content":2428,"level":199},"/docs/api/rest-methods#creating-the-controller","Creating The Controller",[77,2416],"Next, let's take a closer look at a simple API controller that handles this route. We'll leave the show and store methods\nempty for now: \u003C?php\n\nnamespace App\\Http\\Controllers;\n\nuse Binaryk\\LaravelRestify\\Controllers\\RestController;\n\nclass UserController extends RestController\n{\n    /**\n     * Store a newly created user in storage.\n     *\n     * @param  Request  $request\n     * @return Response\n     */\n    public function store(Request $request)\n    {\n        // The only way to do great work is to love what you do.\n    }\n\n    /**\n     * Display the user entity\n     *\n     * @param int $id\n     * @return Response\n     */\n    public function show($id)\n    {\n        // Very little is needed to make a happy life.\n    }\n}",{"id":2430,"title":2431,"titles":2432,"content":2433,"level":199},"/docs/api/rest-methods#writing-the-api-response-logic","Writing The API Response Logic",[77,2416],"Now, we are ready to fill in our show method with the logic to respond with the user resource. To do this, we will use\nthe respond method provided by the parent Binaryk\\LaravelRestify\\Controllers\\RestController class. A JSON response\nwill be sent for API request containing data and errors properties. To get a better understanding of the respond method, let's jump back into the show method: /**\n * Display the user entity\n *\n * @param int $id\n * @return Response\n */\npublic function show($id)\n{\n    return $this->response(User::find($id));\n} As you can see, we pass the desired data into the respond method. This method will wrap the passed data into a JSON\nobject and attach it to the data response's property.",{"id":2435,"title":2436,"titles":2437,"content":2438,"level":199},"/docs/api/rest-methods#receiving-api-response","Receiving API Response",[77,2416],"Once the respond method wraps up the data, the HTTP request will receive back a response with the following structure: {\n  \"data\": {\n    \"id\": 1,\n    \"name\": \"User name\",\n    \"email\": \"kshlerin.hertha@example.com\",\n    \"email_verified_at\": \"2019-12-20 09:48:54\",\n    \"created_at\": \"2019-12-20 09:48:54\",\n    \"updated_at\": \"2020-01-10 12:01:17\"\n  }\n} or: {\n  \"errors\": []\n}",{"id":2440,"title":2441,"titles":2442,"content":2443,"level":188},"/docs/api/rest-methods#response-factory","Response factory",[77],"In addition, the parent RestController provides a powerful response factory method. To understand this, let's return\nback to our store method from the UserController: /**\n * Store a newly created resource in storage.\n *\n * @param Request $request\n * @return Response\n */\npublic function store(Request $request)\n{\n    return $this->response();\n} The response() method will be an instance of Binaryk\\LaravelRestify\\Controllers\\RestResponse. For more information\non working with this object instance,\ncheck out its documentation. $this->response()\n->data($user)\n->message('This is the first user'); The response will look like: {\n  \"data\": {\n    \"id\": 1,\n    \"name\": \"User name\",\n    \"email\": \"kshlerin.hertha@example.com\",\n    \"email_verified_at\": \"2019-12-20 09:48:54\",\n    \"created_at\": \"2019-12-20 09:48:54\",\n    \"updated_at\": \"2020-01-10 12:01:17\"\n  },\n  \"meta\": {\n    \"message\": \"This is the first user\"\n  }\n}",{"id":2445,"title":2446,"titles":2447,"content":2448,"level":199},"/docs/api/rest-methods#displaying-response-errors","Displaying Response Errors",[77,2441],"As we saw above, the response always contains an errors property. This can be either an empty array or a list with\nerrors. For example, what if the incoming request parameters can not pass the given validation rules? This can be handled\nby the errors proxy method: /**\n * Store a newly created resource in storage.\n *\n * @param Request $request\n * @return Response\n */\npublic function store(Request $request)\n{\n    try {\n        $this->validate($request, [\n            'title' => 'required|unique:users|max:255',\n        ]);\n\n        // The user is valid\n    } catch (ValidationException $exception) {\n        // The user is not valid\n        return $this->errors($exception->errors());\n    }\n} The returned API response will have the 400 HTTP code and the following format: {\n  \"errors\": {\n    \"title\": [\n      \"The title field is required.\"\n    ]\n  }\n}",{"id":2450,"title":2451,"titles":2452,"content":2453,"level":188},"/docs/api/rest-methods#custom-header","Custom Header",[77],"Sometimes you may need to respond with a custom header, according to JSON:API.\nAfter storing an entity, we should respond with a Location header that has the value endpoint to the resource: return $this->response()\n    ->header('Location', 'api/users/1')\n    ->data($user);",{"id":2455,"title":2456,"titles":2457,"content":2458,"level":188},"/docs/api/rest-methods#optional-attributes","Optional Attributes",[77],"Therefore, Restify returns the data and errors attributes in the API's response. It also wraps the message into a meta\nobject. But what if we have to send some custom attributes? In addition to generating the default fields, you can add extra\nfields to the response by using the setMeta method from the RestResponse object: return $this->response()\n    ->data($user)\n    ->setMeta('related', [ 'William Shakespeare', 'Agatha Christie', 'Leo Tolstoy' ]);",{"id":2460,"title":2461,"titles":2462,"content":2463,"level":188},"/docs/api/rest-methods#hiding-default-attribute","Hiding Default Attribute",[77],"Restify has a list of predefined attributes, such as: 'line', 'file', 'stack', 'data', 'errors', 'meta'. Some of these are hidden in production though: 'line', 'file', 'stack', since they are only used for tracking exceptions. If you would like the API's response to not contain any of these fields (or hide a specific one like errors, for example),\nthis action can be done by setting in the application provider the: RestResponse::$RESPONSE_DEFAULT_ATTRIBUTES = ['data', 'meta'];",{"id":2465,"title":2466,"titles":2467,"content":2468,"level":188},"/docs/api/rest-methods#rest-response-methods","Rest Response Methods",[77],"The $this->response() returns an instance of Binaryk\\LaravelRestify\\Controllers\\RestResponse. This exposes multiple\nmagical methods for your consistent API response.",{"id":2470,"title":2471,"titles":2472,"content":2473,"level":199},"/docs/api/rest-methods#attach-data","Attach data",[77,2466],"As we have already seen, attaching data to the response can be done by using: ->data($info)",{"id":2475,"title":2476,"titles":2477,"content":2478,"level":199},"/docs/api/rest-methods#headers-setup","Headers setup",[77,2466],"The header could be set by using header method, as it accepts only two arguments - the header's name and header's value: ->header('Location', 'api/users/1')",{"id":2480,"title":2209,"titles":2481,"content":2482,"level":199},"/docs/api/rest-methods#meta-information",[77,2466],"In addition to the data, you may want to send some extra attributes to the client. A message would be a good example, or even anything\nelse for that matter: ->setMeta('name', 'Eduard Lupacescu') ->message(__('Silence is golden.'))",{"id":2484,"title":2485,"titles":2486,"content":2487,"level":188},"/docs/api/rest-methods#response-code-modifiers","Response code modifiers",[77],"Oftentimes, we have to send an informative response code. The following methods are used for setting the code's response:",{"id":2489,"title":2490,"titles":2491,"content":2492,"level":199},"/docs/api/rest-methods#refresh-103","Refresh 103",[77,2485],"->refresh()",{"id":2494,"title":2495,"titles":2496,"content":2497,"level":199},"/docs/api/rest-methods#success-200","Success 200",[77,2485],"->success()",{"id":2499,"title":2500,"titles":2501,"content":2502,"level":199},"/docs/api/rest-methods#created-201","Created 201",[77,2485],"->created()",{"id":2504,"title":2505,"titles":2506,"content":2507,"level":199},"/docs/api/rest-methods#deleted-no-content-204","Deleted (No Content) 204",[77,2485],"->deleted() ->blank()",{"id":2509,"title":2510,"titles":2511,"content":2512,"level":199},"/docs/api/rest-methods#invalid-400","Invalid 400",[77,2485],"->invalid()",{"id":2514,"title":2515,"titles":2516,"content":2517,"level":199},"/docs/api/rest-methods#unauthorized-401","Unauthorized 401",[77,2485],"->unauthorized()",{"id":2519,"title":2520,"titles":2521,"content":2522,"level":199},"/docs/api/rest-methods#forbidden-403","Forbidden 403",[77,2485],"->forbidden()",{"id":2524,"title":2525,"titles":2526,"content":2527,"level":199},"/docs/api/rest-methods#missing-404","Missing 404",[77,2485],"->missing()",{"id":2529,"title":2530,"titles":2531,"content":2532,"level":199},"/docs/api/rest-methods#throttle-429","Throttle 429",[77,2485],"->throttle()",{"id":2534,"title":2535,"titles":2536,"content":2537,"level":199},"/docs/api/rest-methods#unavailable-503","Unavailable 503",[77,2485],"->unavailable()",{"id":2539,"title":2540,"titles":2541,"content":2542,"level":188},"/docs/api/rest-methods#debugging","Debugging",[77],"The following methods could be used to debug some information in the dev mode:",{"id":2544,"title":2545,"titles":2546,"content":2547,"level":199},"/docs/api/rest-methods#line-debugging","Line debugging",[77,2540],"$lineNumber = 201;\n$this->line($lineNumber)",{"id":2549,"title":2550,"titles":2551,"content":2552,"level":199},"/docs/api/rest-methods#debug-to-file","Debug to file",[77,2540],"This could be used for debugging the file's name $this->file($exception->getFile())",{"id":2554,"title":2555,"titles":2556,"content":2557,"level":199},"/docs/api/rest-methods#stack-traces","Stack traces",[77,2540],"With this you can log the exception stach trace $this->stack($exception->getTraceAsString())",{"id":2559,"title":2560,"titles":2561,"content":2562,"level":199},"/docs/api/rest-methods#errors-methods","Errors methods",[77,2540],"The following methods could be used for adding errors to the response:",{"id":2564,"title":2565,"titles":2566,"content":2567,"level":199},"/docs/api/rest-methods#adding-multiple-errors","Adding multiple errors",[77,2540],"Adding a set of errors at once: $this->errors([ 'Something went wrong' ])",{"id":2569,"title":2570,"titles":2571,"content":2572,"level":199},"/docs/api/rest-methods#adderror-function","addError function",[77,2540],"Adding error by error in a response's instance: $this->addError('Something went wrong')",{"id":2574,"title":2575,"titles":2576,"content":2577,"level":188},"/docs/api/rest-methods#custom-paginator","Custom Paginator",[77],"Every so often you have a customed paginator collection. You want to keep the same response format, just as the Repositorydoes. You can use this static call: $paginator = User::query()->paginate(5);\n\n        $response = Binaryk\\LaravelRestify\\Controllers\\RestResponse::index(\n            $paginator\n        ); The $paginator argument should be an instance of: Illuminate\\Pagination\\AbstractPaginator. The expected response will contain: {\n  \"meta\": {\n    \"current_page\": 1,\n    \"from\": 1,\n    \"last_page\": 1,\n    \"path\": \"http://localhost\",\n    \"per_page\": 5,\n    \"to\": 1,\n    \"total\": 1\n  },\n  \"links\": {\n    \"first\": \"http://localhost?page=1\",\n    \"last\": \"http://localhost?page=1\",\n    \"prev\": null,\n    \"next\": null\n  },\n  \"data\": []\n} html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .svl4J, html code.shiki .svl4J{--shiki-light:#F76D47;--shiki-light-font-style:italic;--shiki-default:#F78C6C;--shiki-default-font-style:italic;--shiki-dark:#F78C6C;--shiki-dark-font-style:italic}html pre.shiki code .sFweD, html code.shiki .sFweD{--shiki-light:#E2931D;--shiki-light-font-style:italic;--shiki-default:#FFCB6B;--shiki-default-font-style:italic;--shiki-dark:#FFCB6B;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":82,"title":81,"titles":2579,"content":2580,"level":182},[],"Learn how to use fluent validation methods in Laravel Restify to chain rules like required(), email(), min(), and max() directly on fields for cleaner API code. Laravel Restify provides a fluent API for adding validation rules to fields, similar to Laravel Nova. This makes it easy to chain validation methods for cleaner and more readable code.",{"id":2582,"title":1529,"titles":2583,"content":2584,"level":188},"/docs/api/validation-methods#basic-usage",[81],"Instead of using the traditional rules() method with string rules, you can now use fluent validation methods: // Traditional approach\nfield('email')->rules('required', 'email', 'unique:users');\n\n// Fluent approach\nfield('email')->required()->email()->unique('users');",{"id":2586,"title":2587,"titles":2588,"content":224,"level":188},"/docs/api/validation-methods#available-validation-methods","Available Validation Methods",[81],{"id":2590,"title":2591,"titles":2592,"content":224,"level":199},"/docs/api/validation-methods#common-validation-methods","Common Validation Methods",[81,2587],{"id":2594,"title":2595,"titles":2596,"content":2597,"level":834},"/docs/api/validation-methods#required-and-nullable","Required and Nullable",[81,2587,2591],"field('name')->required();\nfield('bio')->nullable();",{"id":2599,"title":2600,"titles":2601,"content":2602,"level":834},"/docs/api/validation-methods#type-validation","Type Validation",[81,2587,2591],"field('email')->email();\nfield('age')->integer();\nfield('price')->numeric();\nfield('is_active')->boolean();\nfield('tags')->array();\nfield('description')->string();\nfield('metadata')->json();\nfield('website')->url();\nfield('uuid')->uuid();\nfield('birthday')->date();\nfield('created_at')->datetime();",{"id":2604,"title":2605,"titles":2606,"content":2607,"level":834},"/docs/api/validation-methods#numeric-constraints","Numeric Constraints",[81,2587,2591],"field('age')->integer()->min(18)->max(100);\nfield('price')->numeric()->between(0, 99999.99);\nfield('quantity')->integer()->min(0);\nfield('rating')->numeric()->between(1, 5);",{"id":2609,"title":2610,"titles":2611,"content":2612,"level":834},"/docs/api/validation-methods#string-constraints","String Constraints",[81,2587,2591],"field('username')->string()->min(3)->max(20);\nfield('title')->string()->max(255);\nfield('code')->string()->size(6); // Exactly 6 characters",{"id":2614,"title":2615,"titles":2616,"content":2617,"level":834},"/docs/api/validation-methods#password-validation","Password Validation",[81,2587,2591],"field('password')->required()->password()->confirmed();\nfield('password')->required()->password(10); // Minimum 10 characters",{"id":2619,"title":2620,"titles":2621,"content":2622,"level":834},"/docs/api/validation-methods#unique-and-exists-validation","Unique and Exists Validation",[81,2587,2591],"// Basic unique validation\nfield('email')->unique('users');\n\n// Unique with custom column\nfield('slug')->unique('posts', 'slug');\n\n// Unique with ignore (useful for updates)\nfield('email')->unique('users', 'email', $userId);\n\n// Exists validation\nfield('category_id')->exists('categories', 'id');\nfield('user_id')->required()->exists('users');",{"id":2624,"title":2625,"titles":2626,"content":2627,"level":834},"/docs/api/validation-methods#date-validation","Date Validation",[81,2587,2591],"field('start_date')->date()->after('today');\nfield('end_date')->date()->afterOrEqual('start_date');\nfield('birth_date')->date()->before('today');\nfield('expired_at')->datetime()->beforeOrEqual('2024-12-31');\nfield('scheduled_at')->datetime()->dateFormat('Y-m-d H:i:s');",{"id":2629,"title":2630,"titles":2631,"content":2632,"level":834},"/docs/api/validation-methods#file-validation","File Validation",[81,2587,2591],"field('document')->isFile()->max(5120); // 5MB\nfield('avatar')->isImage()->max(2048); // 2MB",{"id":2634,"title":2635,"titles":2636,"content":2637,"level":834},"/docs/api/validation-methods#pattern-validation","Pattern Validation",[81,2587,2591],"field('phone')->regex('/^[0-9]{10}$/');\nfield('username')->alphaDash(); // Letters, numbers, dashes, underscores\nfield('name')->alpha(); // Letters only\nfield('code')->alphaNum(); // Letters and numbers only",{"id":2639,"title":2640,"titles":2641,"content":2642,"level":834},"/docs/api/validation-methods#ip-address-validation","IP Address Validation",[81,2587,2591],"field('ip')->ip();\nfield('ipv4')->ipv4();\nfield('ipv6')->ipv6();",{"id":2644,"title":2645,"titles":2646,"content":2647,"level":834},"/docs/api/validation-methods#conditional-validation","Conditional Validation",[81,2587,2591],"// Required if another field has a specific value\nfield('phone')->requiredIf('contact_method', 'phone');\n\n// Required unless another field has a specific value\nfield('reason')->requiredUnless('status', 'approved');\n\n// Required with other fields\nfield('password_confirmation')->requiredWith('password');\n\n// Required with all specified fields\nfield('state')->requiredWithAll(['country', 'city']);\n\n// Required without other fields\nfield('email')->requiredWithout('phone');",{"id":2649,"title":2650,"titles":2651,"content":2652,"level":834},"/docs/api/validation-methods#innot-in-validation","In/Not In Validation",[81,2587,2591],"field('status')->in(['pending', 'approved', 'rejected']);\nfield('role')->notIn(['admin', 'super-admin']);",{"id":2654,"title":2655,"titles":2656,"content":2657,"level":834},"/docs/api/validation-methods#other-useful-methods","Other Useful Methods",[81,2587,2591],"// Field must be accepted (yes, on, 1, or true)\nfield('terms')->accepted();\n\n// Field must be confirmed (field_confirmation must exist)\nfield('email')->confirmed();\n\n// Field must be different from another field\nfield('new_password')->different('current_password');\n\n// Field must be the same as another field\nfield('password_confirmation')->same('password');\n\n// Field must match current user's password\nfield('current_password')->currentPassword();\n\n// Field must be filled if present\nfield('description')->filled();\n\n// Field must be present in request\nfield('token')->present();\n\n// String must start/end with specific values\nfield('url')->startsWith(['http://', 'https://']);\nfield('filename')->endsWith(['.jpg', '.png', '.pdf']);\n\n// Timezone validation\nfield('timezone')->timezone();\n\n// MAC address validation\nfield('mac')->macAddress();\n\n// Multiple of value\nfield('quantity')->integer()->multipleOf(5);",{"id":2659,"title":2660,"titles":2661,"content":224,"level":188},"/docs/api/validation-methods#complex-examples","Complex Examples",[81],{"id":2663,"title":2664,"titles":2665,"content":2666,"level":199},"/docs/api/validation-methods#user-registration-form","User Registration Form",[81,2660],"public function fields(RestifyRequest $request)\n{\n    return [\n        field('name')->required()->string()->min(2)->max(100),\n        \n        field('email')->required()->email()->unique('users'),\n        \n        field('username')\n            ->required()\n            ->string()\n            ->min(3)\n            ->max(20)\n            ->alphaDash()\n            ->unique('users'),\n        \n        field('password')\n            ->required()\n            ->password()\n            ->confirmed(),\n        \n        field('age')->nullable()->integer()->min(13)->max(120),\n        \n        field('terms_accepted')->required()->accepted(),\n        \n        field('notification_email')\n            ->requiredIf('receive_notifications', true)\n            ->email(),\n    ];\n}",{"id":2668,"title":2669,"titles":2670,"content":2671,"level":199},"/docs/api/validation-methods#product-form","Product Form",[81,2660],"public function fields(RestifyRequest $request)\n{\n    return [\n        field('name')->required()->string()->max(255),\n        \n        field('slug')->required()->string()->unique('products'),\n        \n        field('price')->required()->numeric()->min(0)->max(999999.99),\n        \n        field('sale_price')->nullable()->numeric()->between(0, 999999.99),\n        \n        field('sku')->required()->string()->size(8)->unique('products'),\n        \n        field('status')->required()->in(['draft', 'published', 'archived']),\n        \n        field('published_at')\n            ->nullable()\n            ->datetime()\n            ->afterOrEqual('today'),\n        \n        field('category_id')->required()->exists('categories', 'id'),\n        \n        field('weight')->nullable()->numeric()->min(0),\n        \n        field('is_featured')->boolean(),\n    ];\n}",{"id":2673,"title":2674,"titles":2675,"content":2676,"level":199},"/docs/api/validation-methods#article-form-with-conditional-rules","Article Form with Conditional Rules",[81,2660],"public function fields(RestifyRequest $request)\n{\n    return [\n        field('title')->required()->string()->max(200),\n        \n        field('content')->required()->string()->min(100),\n        \n        field('excerpt')\n            ->requiredUnless('auto_excerpt', true)\n            ->string()\n            ->max(500),\n        \n        field('featured_image')\n            ->requiredIf('is_featured', true)\n            ->isImage()\n            ->max(5120),\n        \n        field('publish_date')\n            ->requiredIf('status', 'published')\n            ->date()\n            ->afterOrEqual('today'),\n        \n        field('tags')->nullable()->array(),\n        \n        field('meta_description')\n            ->nullable()\n            ->string()\n            ->between(50, 160),\n    ];\n}",{"id":2678,"title":2679,"titles":2680,"content":2681,"level":188},"/docs/api/validation-methods#combining-with-traditional-rules","Combining with Traditional Rules",[81],"You can still combine fluent methods with the traditional rules() method when needed: field('email')\n    ->required()\n    ->email()\n    ->rules('unique:users,email,' . $userId)\n    ->rules(new CustomEmailRule);",{"id":2683,"title":2684,"titles":2685,"content":2686,"level":188},"/docs/api/validation-methods#custom-validation-messages","Custom Validation Messages",[81],"Validation methods can be combined with custom messages: field('email')\n    ->required()\n    ->email()\n    ->unique('users')\n    ->messages([\n        'required' => 'Email address is required.',\n        'email' => 'Please provide a valid email address.',\n        'unique' => 'This email is already registered.',\n    ]); html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":86,"title":85,"titles":2688,"content":2689,"level":182},[],"Learn how Laravel Restify Actions let you define extra repository operations as invokable classes, improving testability and code maintainability.",{"id":2691,"title":796,"titles":2692,"content":2693,"level":188},"/docs/api/actions#motivation",[85],"Built in CRUD operations and filtering, Restify allows you to define extra actions for your repositories. Let's say you have a list of posts and you have to publish them. Usually, for these kind of operations, you have to define a custom route like: $router->post('posts/publish', PublishPostsController::class);\n\npublic function __invoke(RestifyRequest $request)\n{\n  //...\n} The classic approach is good. However, it has a few limitations. First, you have to manually take care of the middleware route, as the testability for these endpoints should be done separately, which might be hard to maintain. Ultimately, the endpoint is disconnected from the repository, which makes it feel out of context so it has a bad readability. On that wise, code readability, testability and maintainability may become hard.",{"id":2695,"title":2696,"titles":2697,"content":2698,"level":188},"/docs/api/actions#invokable-action-format","Invokable Action Format",[85],"The simplest way to define an action is to use the invokable class format. Here's an example: namespace App\\Restify\\Actions;\n\nuse Illuminate\\Http\\Request;\n\nclass PublishPostAction\n{\n    public function __invoke(Request $request)\n    {\n        // $request->input(...)\n        \n        return response()->json([\n            'message' => 'Post published successfully',\n        ]);\n    }\n} Then add the action instance to the repository actions method: ...\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        new PublishPostAction,\n    ];\n}\n... Bellow we will see how to define actions in a more advanced way.",{"id":2700,"title":2701,"titles":2702,"content":2703,"level":188},"/docs/api/actions#action-definition","Action definition",[85],"The action is nothing more than a class that extends the Binaryk\\LaravelRestify\\Actions\\Action abstract class. It could be generated by using the following command: php artisan restify:action PublishPostsAction This will generate the action class: namespace App\\Restify\\Actions;\n\nuse Binaryk\\LaravelRestify\\Actions\\Action;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\ActionRequest;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Support\\Collection;\n\nclass PublishPostAction extends Action\n{\n    public function handle(ActionRequest $request, Collection $models): JsonResponse\n    {\n        return response()->json();\n    }\n} The $models argument represents a collection of all the models for this query.",{"id":2705,"title":2706,"titles":2707,"content":2708,"level":199},"/docs/api/actions#register-action","Register action",[85,2701],"Then add the action instance to the repository actions method: public function actions(RestifyRequest $request): array\n{\n    return [\n        PublishPostAction::new();\n    ];\n}",{"id":2710,"title":2711,"titles":2712,"content":2713,"level":199},"/docs/api/actions#authorize-action","Authorize action",[85,2701],"You can authorize certain actions to be active for specific users: public function actions(RestifyRequest $request): array\n{\n    return [\n        PublishPostAction::new()->canSee(function (Request $request) {\n            return $request->user()->can('publishAnyPost', Post::class);\n        }),\n    ];\n}",{"id":2715,"title":2716,"titles":2717,"content":2718,"level":199},"/docs/api/actions#call-actions","Call actions",[85,2701],"To call an action, you simply access: POST: api/restify/posts/actions?action=publish-posts-action The action query param value is the ke-bab form of the filter class name by default, or a custom $uriKey defined in the action The payload could be any type of json data. However, if you're using an index-action, you are required to pass the repositories key, which represents the list of model keys that we apply to this action: {\n  \"repositories\": [1, 2]\n}",{"id":2720,"title":2721,"titles":2722,"content":2723,"level":199},"/docs/api/actions#handle-action","Handle action",[85,2701],"As soon the action is called, the handled method will be invoked with the $request and a list of models matching the keys passed via repositories: public function handle(ActionRequest $request, Collection $models)\n{\n    $models->each->publish();\n\n    return ok();\n}",{"id":2725,"title":2726,"titles":2727,"content":2728,"level":188},"/docs/api/actions#action-customizations","Action customizations",[85],"Actions can be easily customized.",{"id":2730,"title":2731,"titles":2732,"content":2733,"level":199},"/docs/api/actions#action-index-query","Action index query",[85,2726],"Similarly to repository index query, we can do the same by adding the indexQuery method on the action: class PublishPostAction extends Action\n{\n    public static function indexQuery(RestifyRequest $request, $query)\n    {\n        $query->whereNotNull('published_at');\n    }\n    \n    ...\n} This method will be called right before items are retrieved from the database, so you can filter out or eager load using your custom statements.",{"id":2735,"title":2736,"titles":2737,"content":2738,"level":199},"/docs/api/actions#custom-uri-key","Custom uri key",[85,2726],"Since your class names might change along the way, you can define a $uriKey property to your actions, so the frontend will always use the same action query when applying an action: class PublishPostAction extends Action\n{\n    public static $uriKey = 'publish-posts';\n\n    //...\n\n};",{"id":2740,"title":2741,"titles":2742,"content":2743,"level":199},"/docs/api/actions#rules","Rules",[85,2726],"Similarly to advanced filters rules, you could define rules for the action so the payload will get validated before the handle method is fired. public function rules(): array\n{\n    return [\n        'active' => ['required', 'bool'],\n    ];\n} Restify doesn't validate the payload automatically as it does for filters, so you're free to validate the payload in the handle method. Always validate the payload as early as possible in the handle method: public function handle(ActionRequest $request, Collection $models)\n{\n    $request->validate($this->rules());\n\n    ...\n}",{"id":2745,"title":847,"titles":2746,"content":2747,"level":199},"/docs/api/actions#mcp-server-integration",[85,2726],"When using Laravel Restify with the Model Context Protocol (MCP), actions are automatically exposed as tools to AI agents. You can enhance the AI's understanding of your actions by providing descriptions and validation rules.",{"id":2749,"title":2750,"titles":2751,"content":2752,"level":834},"/docs/api/actions#action-description","Action Description",[85,2726,847],"Provide a clear description of what your action does by setting the description property or method. This helps AI agents understand when and how to use your action: class PublishPostAction extends Action\n{\n    public string $description = 'Publish selected posts and notify authors via email';\n\n    // Or override the method for dynamic descriptions\n    public function description(RestifyRequest $request): string\n    {\n        return 'Publish selected posts and notify authors via email';\n    }\n\n    //...\n}",{"id":2754,"title":857,"titles":2755,"content":2756,"level":834},"/docs/api/actions#validation-rules-for-ai-schema",[85,2726,847],"The rules() method is crucial for MCP integration. Restify automatically converts your Laravel validation rules into JSON Schema that AI agents can understand. This allows the AI to validate parameters before executing the action: class PublishPostAction extends Action\n{\n    public string $description = 'Publish selected posts with optional scheduling';\n\n    public function rules(): array\n    {\n        return [\n            'notify_authors' => ['boolean'],\n            'publish_date' => ['nullable', 'date', 'after:now'],\n            'notification_message' => ['nullable', 'string', 'max:500'],\n        ];\n    }\n\n    public function handle(ActionRequest $request, Collection $models)\n    {\n        $request->validate($this->rules());\n\n        // Action implementation\n    }\n} The AI agent will automatically receive a JSON Schema indicating: notify_authors: boolean (optional)publish_date: date string (optional, must be in the future)notification_message: string (optional, max 500 characters) This schema generation works with 60+ Laravel validation rules including: email, url, uuid, integer, min, max, between, before, after, in, array, and many more.",{"id":2758,"title":2759,"titles":2760,"content":2761,"level":188},"/docs/api/actions#actions-scope","Actions scope",[85],"By default, any action could be used on index as well as on show. However, you can choose to instruct your action to be displayed to a specific scope.",{"id":2763,"title":2764,"titles":2765,"content":2766,"level":188},"/docs/api/actions#show-actions","Show actions",[85],"Show actions are used when you have to apply them for a single item.",{"id":2768,"title":2769,"titles":2770,"content":2771,"level":199},"/docs/api/actions#show-action-definition","Show action definition",[85,2764],"The show action definition is different, in a way it receives arguments for the handle method. Restify automatically resolves Eloquent models defined in the route id and passes them to the action's handle method: public function handle(ActionRequest $request, Post $post): JsonResponse\n{\n\n}",{"id":2773,"title":2774,"titles":2775,"content":2776,"level":199},"/docs/api/actions#show-action-registration","Show action registration",[85,2764],"To register a show action, we have to use the ->onlyOnShow() accessor: public function actions(RestifyRequest $request)\n{\n    return [\n        PublishPostAction::new()->onlyOnShow(),\n    ];\n}",{"id":2778,"title":2779,"titles":2780,"content":2781,"level":199},"/docs/api/actions#show-action-call","Show action call",[85,2764],"The post URL should include the key of the model we want Restify to resolve: POST: api/restfiy/posts/1/actions?action=publish-post-action The payload could be empty: {}",{"id":2783,"title":2784,"titles":2785,"content":2786,"level":199},"/docs/api/actions#list-show-actions","List show actions",[85,2764],"To get the list of available actions only for a specific model key: GET: api/api/restify/posts/1/actions See get available actions for more details.",{"id":2788,"title":2789,"titles":2790,"content":2791,"level":188},"/docs/api/actions#index-actions","Index actions",[85],"Index actions are used when you have to apply them for a many items.",{"id":2793,"title":2794,"titles":2795,"content":2796,"level":199},"/docs/api/actions#index-action-definition","Index action definition",[85,2789],"The index action definition is different in the way it receives arguments for the handle method. Restify automatically resolves Eloquent models sent via the repositories key into the call payload. Then, it passes it to the action's handle method as a collection of items: use Illuminate\\Support\\Collection;\n\npublic function handle(ActionRequest $request, Collection $posts): JsonResponse\n{\n    //\n}",{"id":2798,"title":2799,"titles":2800,"content":2801,"level":199},"/docs/api/actions#index-action-registration","Index action registration",[85,2789],"To register an index action, we have to use the ->onlyOnIndex() accessor: public function actions(RestifyRequest $request)\n{\n    return [\n        PublishPostsAction::new()->onlyOnIndex(),\n    ];\n}",{"id":2803,"title":2804,"titles":2805,"content":2806,"level":199},"/docs/api/actions#index-action-call","Index action call",[85,2789],"The post URL: POST: api/restfiy/posts/actions?action=publish-posts-action The payload should always include a key called repositories, which is an array of model keys or the all keyword if you want to get them all: {\n  \"repositories\": [1, 2, 3]\n} So Restify will resolve posts with ids in the list of [1, 2, 3].",{"id":2808,"title":2809,"titles":2810,"content":2811,"level":199},"/docs/api/actions#apply-index-action-for-all","Apply index action for all",[85,2789],"You can apply the index action for all the models from the database if you send the payload: {\n  \"repositories\": \"all\"\n} Restify will get chunks of 200 and send them into the Collection argument for the handle method. You can customize the chunk number by customizing the chunkCount action property: public static int $chunkCount = 500;",{"id":2813,"title":2814,"titles":2815,"content":2816,"level":199},"/docs/api/actions#list-index-actions","List index actions",[85,2789],"To get the list of available actions: GET: api/api/restify/posts/actions See get available actions for more details.",{"id":2818,"title":2819,"titles":2820,"content":2821,"level":188},"/docs/api/actions#standalone-actions","Standalone actions",[85],"Sometimes, you don't need to have an action with models. Let's say for example the authenticated user wants to disable\nhis/her account.",{"id":2823,"title":2824,"titles":2825,"content":2826,"level":199},"/docs/api/actions#standalone-action-definition","Standalone action definition:",[85,2819],"The index action definition is different, in a way it doesn't require the second argument for the handle. public function handle(ActionRequest $request): JsonResponse\n{\n    //\n}",{"id":2828,"title":2829,"titles":2830,"content":2831,"level":199},"/docs/api/actions#standalone-action-registration","Standalone action registration",[85,2819],"There are two ways to register the standalone action: public function actions(RestifyRequest $request)\n{\n    return [\n        DisableProfileAction::new()->standalone(),\n    ];\n} Using the ->standalone() mutator or by overriding the $standalone action property directly into the action: class DisableProfileAction extends Action\n{\n    public bool $standalone = true;\n\n    //...\n}",{"id":2833,"title":2834,"titles":2835,"content":2836,"level":199},"/docs/api/actions#standalone-action-call","Standalone action call",[85,2819],"To call a standalone action you're using a similar URL as for the index action POST: api/restfiy/users/actions?action=disable-profile-action However, you are not required to pass the repositories payload key.",{"id":2838,"title":2839,"titles":2840,"content":2841,"level":199},"/docs/api/actions#list-standalone-actions","List standalone actions",[85,2819],"Standalone actions will be displayed on both listing show actions or listing index actions.",{"id":2843,"title":2844,"titles":2845,"content":2846,"level":188},"/docs/api/actions#filters","Filters",[85],"You can apply any search, match, filter or eager loadings as for a usual request: POST: api/api/restify/posts/actions?action=publish-posts-action&id=1&filters= This will apply the match for the id = 1 and filter along with the match for the repositories payload you're\nsending.",{"id":2848,"title":2849,"titles":2850,"content":2851,"level":188},"/docs/api/actions#action-log","Action Log",[85],"Oftentimes, it is quite useful to view a log of the actions that have been run against a model, or see when the model was\nupdated, deleted or created (and by whom). Thankfully, Restify makes it a breeze to add an action log to a model by attaching the Binaryk\\LaravelRestify\\Models\\Concerns\\HasActionLogs trait to the repository's corresponding Eloquent model.",{"id":2853,"title":2854,"titles":2855,"content":2856,"level":199},"/docs/api/actions#activate-logs","Activate logs",[85,2849],"By simply adding the HasActionLogs trait to your model, it will log all actions and CRUD operations into the database into the action_logs table: class Post extends Model \n{\n    use \\Binaryk\\LaravelRestify\\Models\\Concerns\\HasActionLogs;\n}",{"id":2858,"title":2859,"titles":2860,"content":2861,"level":199},"/docs/api/actions#display-logs","Display logs",[85,2849],"You can display them by attaching them to the related repository for example: use Binaryk\\LaravelRestify\\Fields\\MorphToMany;\nuse Binaryk\\LaravelRestify\\Repositories\\ActionLogRepository;\n\npublic static function related(): array\n{\n    return [\n        'logs' => MorphToMany::make('actionLogs', ActionLogRepository::class),\n    ];\n} Now you can call the posts with logs api/restify/posts/1?related=logs, and it will return you the list of actions\nperformed for posts: [\n  {\n    \"id\": \"1\",\n    \"type\": \"action_logs\",\n    \"attributes\": {\n      \"user_id\": \"1\",\n      \"name\": \"Stored\",\n      \"actionable_type\": \"App\\\\Models\\\\Post\",\n      \"actionable_id\": \"1\",\n      \"status\": \"finished\",\n      \"original\": [],\n      \"changes\": [],\n      \"exception\": \"\"\n    }\n  }\n]",{"id":2863,"title":2864,"titles":2865,"content":2866,"level":199},"/docs/api/actions#custom-logs-repository","Custom logs repository",[85,2849],"You can definitely use your own ActionLogRepository. Just make sure you have it defined into the config: ...\n'logs' => [\n    'repository' => MyCustomLogsRepository::class,\n],",{"id":2868,"title":2869,"titles":2870,"content":2871,"level":188},"/docs/api/actions#get-available-actions","Get available actions",[85],"The frontend that consumes your API could check available actions by using this exposed endpoint: GET: api/api/restify/posts/actions This will answer with a json like: {\n  \"data\": {\n    \"name\": \"Publish Posts Action\",\n    \"destructive\": false,\n    \"uriKey\": \"publish-posts-action\",\n    \"payload\": []\n  }\n} name - humanized name of the action destructive - you may extend the Binaryk\\LaravelRestify\\Actions\\DestructiveAction to indicate to the frontend that\nthis action is destructive (could be used for deletions) uriKey - is the key of the action and it will be used to perform the action payload - a key / value object indicating required payload defined in the rules Action class html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":95,"title":94,"titles":2873,"content":2874,"level":182},[],"Learn how to use global search and basic filters in Laravel Restify. Define searchable fields, paginate results, and integrate Laravel Scout. Restify provides few powerful ways to filter and search your data.",{"id":2876,"title":2877,"titles":2878,"content":2879,"level":188},"/docs/search/basic-filters#global-search","Global search",[94],"Restify provides a global endpoint that searches over all repositories searchable fields. To define which repository fields are searchable, you may assign an array of database columns in the search property of your repository class. This includes id column by default, but you may override it to your needs: class PostRepository extends Repository\n{\n    public static array $search = ['id', 'title']; The endpoint to search is: GET: /api/restify/search?search=\"Test title\" It will search over all repositories that are authorized (has allowRestify policy on true).",{"id":2881,"title":2882,"titles":2883,"content":2884,"level":199},"/docs/search/basic-filters#disabling-global-search","Disabling global search",[94,2877],"There are 2 ways to disable the global search:  for a repository, either return false from the allowRestify model policy method or So to disable the Posts from the global search using the repository property we do: public static bool $globallySearchable = false;",{"id":2886,"title":2887,"titles":2888,"content":2889,"level":199},"/docs/search/basic-filters#paginate-global-search","Paginate global search",[94,2877],"You can limit the number of results that are returned in the global search by overriding the globalSearchResults property on the resource: public static int $globalSearchResults = 5; Restify has built in support for laravel scout, so it will initialize the query using scout if you have setup it for the model.",{"id":2891,"title":2892,"titles":2893,"content":2894,"level":199},"/docs/search/basic-filters#customize-global-search","Customize global search",[94,2877],"The default global search response looks like this: {\n  \"data\": [\n    {\n      \"repositoryName\": \"users\",\n      \"repositoryTitle\": \"Users\",\n      \"title\": \"Mrs. Lucie Parker Jr.\",\n      \"subTitle\": null,\n      \"repositoryId\": 1,\n      \"link\": \"/api/restify/users/1\"\n    }\n  ]\n} Where the title is the repository column defined by the $title property. So you can customize it: public static string $title = 'email'; The subTitle could be customized by overriding the subtitle method. The returned value will be displayed here: public function subtitle(): ?string\n{\n    return 'User email: ' . $this->model()->email;\n} The repositoryTitle could be customized by overriding the label method or by defining the $label property. This will customize the displayed repository title in the global search response: ```php [UserRepository.php]\npublic static string $label = 'Users';\n\n// Or using the label method for dynamic titles:\npublic static function label(): string\n{\n    return 'Dynamic Title';\n}",{"id":2896,"title":2897,"titles":2898,"content":2899,"level":188},"/docs/search/basic-filters#repository-search","Repository Search",[94],"The repository search works in a similar way as global search, however in this case the endpoint refers to the repository and the search will be applied for a certain repository. Say we want to search users by their email and name: class UserRepository extends Repository\n{\n    public static array $search = ['name', 'email']; So the endpoint will scope the the users repository now: GET: /api/restify/users?search=\"John Doe\"",{"id":2901,"title":2902,"titles":2903,"content":2904,"level":188},"/docs/search/basic-filters#case-sensitive-search","Case-sensitive search",[94],"By default, Restify search is case-sensitive. You can change this behavior globally: 'search' => [\n    /*\n    | Specify either the search should be case-sensitive or not.\n    */\n    'case_sensitive' => false,\n], When case_sensitive is false, every searchable column is wrapped in UPPER(): WHERE UPPER(posts.title) LIKE UPPER('%foo%') This matches case-insensitively at the cost of bypassing any column index. For columns where you know the stored case (numeric, FK ids, codes stored uppercase, status enums stored lowercase), you can opt into index-friendly modes per column.",{"id":2906,"title":2907,"titles":2908,"content":2909,"level":199},"/docs/search/basic-filters#per-field-case-modes","Per-field case modes",[94,2902],"SearchableFilter exposes chainable methods to control how the column expression and the search value are transformed for the LIKE comparison. Mix them in the searchables() array: use Binaryk\\LaravelRestify\\Filters\\SearchableFilter;\n\npublic static function searchables(): array\n{\n    return [\n        SearchableFilter::make('gross_amount')->caseRaw(),       // numeric\n        SearchableFilter::make('vendor_code')->upperValue(),     // stored uppercase\n        SearchableFilter::make('status')->lowerValue(),          // stored lowercase\n        SearchableFilter::make('description')->upperBoth(),      // human text\n    ];\n} Available modes: MethodColumnValueUse when->caseRaw()rawrawnumerics, FK ids, dates, blind-indexed->upperValue()rawUPPERcolumn always stored UPPER (e.g. vendor codes)->lowerValue()rawLOWERcolumn always stored lowercase (e.g. status enums)->upperBoth()UPPERUPPERlegacy case-insensitive (column case unknown)->lowerBoth()LOWERLOWERmirror of upperBoth *Value modes leave the column expression untouched, so any index on that column is still usable. *Both modes wrap both sides — case-insensitive but kills indexes. upperValue and lowerValue are silent contracts: they assume the column is always stored in that case. Rows stored in mixed/opposite case will not match the search.",{"id":2911,"title":2912,"titles":2913,"content":2914,"level":199},"/docs/search/basic-filters#custom-value-transformer","Custom value transformer",[94,2902],"For normalization beyond simple case (trim, unicode fold, digit-strip, etc.), pass any callable to transform(): SearchableFilter::make('email')->transform(\n    fn (string $v): string => trim(strtolower($v))\n),\nSearchableFilter::make('phone')->transform(\n    fn (string $v): string => preg_replace('/\\D/', '', $v)\n), The column stays raw; the callable controls the value before the LIKE comparison.",{"id":2916,"title":2917,"titles":2918,"content":2919,"level":199},"/docs/search/basic-filters#per-field-overrides-on-belongsto-searchables","Per-field overrides on BelongsTo searchables",[94,2902],"BelongsTo::->searchable([...]) accepts a mix of plain strings and SearchableFilter instances, so per-column case modes work for joined searchables too: 'vendor' => BelongsTo::make('vendor', VendorRepository::class)->searchable([\n    SearchableFilter::make('vendors.code')->upperValue(),  // index-friendly\n    'vendors.name',                                          // default behavior\n]),",{"id":2921,"title":2922,"titles":2923,"content":2924,"level":199},"/docs/search/basic-filters#resolution-order","Resolution order",[94,2902],"Per searchable column, the resolution is: Instance method (caseRaw, upperValue, transform, etc.) on the SearchableFilter — wins.Otherwise, derive from restify.search.case_sensitive — true → raw, false → UPPER both.",{"id":2926,"title":2927,"titles":2928,"content":2929,"level":199},"/docs/search/basic-filters#custom-search-filter","Custom search filter",[94,2902],"The search could be customized by creating a class that extends the \\Binaryk\\LaravelRestify\\Filters\\SearchableFilter: use Binaryk\\LaravelRestify\\Filters\\SearchableFilter;\n\nclass CustomTitleSearchFilter extends SearchableFilter\n{\n    public function filter(RestifyRequest $request, $query, $value)\n    {\n          return $query->orWhere('name', 'like', \"%$value%\");\n    }\n} In the filter method you can define your own filtering over the $query builder and then attach the class instance to a column: public static function searchables(): array\n{\n    return [\n        'title' => CustomTitleSearchFilter::make(),\n    ];\n} As soon as you define the searchables method into the repository, the $search array is not taken into consideration anymore. So make sure you return all available search fields from this method.",{"id":2931,"title":2932,"titles":2933,"content":2934,"level":188},"/docs/search/basic-filters#match","Match",[94],"Matching by specific attributes may be useful if you want an exact matching. Repository configuration: class PostRepository extends Repository\n{\n    public static array $match = [\n        'id' => 'int',\n        'title' => 'string',\n    ];\n} As we may notice the match configuration is an associative array, defining the attribute name and type mapping. Available types: text (or string)boolint (or integer)datetimebetweenarray When performing the request you may pass the match field and value as query params: GET: /api/restify/posts?id=1 or by title: GET: /api/restify/posts?title=\"Some title\"",{"id":2936,"title":2937,"titles":2938,"content":2939,"level":199},"/docs/search/basic-filters#match-string","Match string",[94,2932],"Definition: class PostRepository extends Repository\n{\n    public static array $match = [\n        'title' => 'string',\n    ];\n} Request: GET: /api/restify/posts?title=\"Title\"",{"id":2941,"title":2942,"titles":2943,"content":2944,"level":199},"/docs/search/basic-filters#match-bool","Match bool",[94,2932],"Definition: class PostRepository extends Repository\n{\n    public static array $match = [\n        'active' => 'bool',\n    ];\n} Request: GET: /api/restify/posts?active=true",{"id":2946,"title":2947,"titles":2948,"content":2949,"level":199},"/docs/search/basic-filters#match-int","Match int",[94,2932],"Definition: class PostRepository extends Repository\n{\n    public static array $match = [\n        'id' => 'int',\n    ];\n} Request: GET: /api/restify/posts?id=1",{"id":2951,"title":2952,"titles":2953,"content":2954,"level":199},"/docs/search/basic-filters#match-datetime","Match datetime",[94,2932],"The datetime filter add behind the scene an whereDate query. class PostRepository extends Repository\n{\n    public static array $match = [\n        'published_at' => 'datetime',\n    ];\n} Request: GET: /api/restify/posts?published_at=2020-12-01 If the request contains two dates instead of one, it will perform a whereBetween query: GET: /api/restify/posts?published_at=2020-12-01,2021-01-01 Eloquent will do: $query->whereBetween('published_at', ['2020-12-01', '2021-01-01']);",{"id":2956,"title":2957,"titles":2958,"content":2959,"level":199},"/docs/search/basic-filters#match-between","Match between",[94,2932],"The between match works similarly as the whereBetween Eloquent method: class PostRepository extends Repository\n{\n    public static array $match = [\n        'id' => 'between',\n        'published_at' => 'between',\n    ];\n} Request: GET: /api/restify/posts?published_at=2021-09-16,2021-11-16 So it will return all posts published between the first and the second dates. It works with integer as well: GET: /api/restify/posts?id=1,20 Match all available ids between 1 and 20.",{"id":2961,"title":2962,"titles":2963,"content":2964,"level":199},"/docs/search/basic-filters#match-array","Match array",[94,2932],"Match also accept a list of elements in the query param: class PostRepository extends Repository\n{\n    public static $match = [\n        'id' => 'array'\n    ];\n} Request: GET: /api/restify/posts?id=1,2,3 This will be converted to: ->whereIn('id', [1, 2, 3])",{"id":2966,"title":2967,"titles":2968,"content":2969,"level":199},"/docs/search/basic-filters#match-null","Match null",[94,2932],"All match types accept null as a value, and check add whereNull to the query: GET: /api/restify/posts?published_at=null",{"id":2971,"title":2972,"titles":2973,"content":2974,"level":199},"/docs/search/basic-filters#match-negation","Match negation",[94,2932],"All match types accept a negation, so you can negate the column match by simply adding the - (minus) sign before the field: GET: /api/restify/posts?-id=1,2,3 This will return all posts where doesn't have the id in the [1,2,3] list. You can apply - (negation) for every match: GET: /api/restify/posts?-title=\"Some title\" This will return all posts that doesn't contain Some title substring.",{"id":2976,"title":2977,"titles":2978,"content":2979,"level":199},"/docs/search/basic-filters#custom-match-filter","Custom match filter",[94,2932],"Sometimes you may have a large logic into a match. To allow this, Restify provides a declarative way to define matchers. For this purpose you should define a class, that extends the Binaryk\\LaravelRestify\\Filters\\MatchFilter: use Binaryk\\LaravelRestify\\Filters\\MatchFilter;\n\nclass ActivePostMatchFiler extends MatchFilter\n{\n    public function filter(RestifyRequest $request, Builder | Relation $query, $value)\n    {\n        // your logic here\n    }\n} The next step is to return this class instance from the matchers method: public static function matches(): array\n{\n    return [\n        'active' => ActivePostMatchFiler::make(),\n    ];\n} As soon as you define the matches method into the repository, the $match array is not taken into consideration anymore. So make sure you return all available matches from this method.",{"id":2981,"title":2982,"titles":2983,"content":2984,"level":199},"/docs/search/basic-filters#partial-match","Partial match",[94,2932],"The match filters 1:1 match, however, when you're looking for a substring into a text, you might need to partially match it. This could be done using the Binaryk\\LaravelRestify\\Filters\\MatchFilter class: public static function matches(): array\n{\n    return [\n        'title' => MatchFilter::make()->setType('text')->partial()\n    ];\n}",{"id":2986,"title":2987,"titles":2988,"content":2989,"level":199},"/docs/search/basic-filters#get-available-matches","Get available matches",[94,2932],"You can use the following request to get all repository matches: /api/restify/posts/filters?only=matches html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":99,"title":98,"titles":2991,"content":2992,"level":182},[],"Build custom Advanced Filters in Laravel Restify by extending AdvancedFilter — define rules, authorize access, and filter Eloquent queries your way. Restify has base filters for usual search or matching. Advanced filters will help you to build your own filters from scratch.",{"id":2994,"title":2164,"titles":2995,"content":2996,"level":188},"/docs/search/advanced-filters#definition",[98],"To declare an advanced filter you should create a class that extends the Binaryk\\LaravelRestify\\Filters\\AdvancedFilter. Say we have a filter that filters all ready to publish posts: use Binaryk\\LaravelRestify\\Filters\\AdvancedFilter;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\nclass ReadyPostsFilter extends AdvancedFilter \n{\n    public function filter(RestifyRequest $request, Relation|Builder $query, $value)\n    {\n        // TODO: Implement filter() method.\n    }\n\n    public function rules(Request $request): array\n    {\n        return [];\n    }\n\n};",{"id":2998,"title":2999,"titles":3000,"content":3001,"level":199},"/docs/search/advanced-filters#register-filter","Register filter",[98,2164],"Then add the filter to the repository filters method: public function filters(RestifyRequest $request): array\n{\n    return [\n        ReadyPostsFilter::new(),\n    ];\n}",{"id":3003,"title":3004,"titles":3005,"content":3006,"level":199},"/docs/search/advanced-filters#authorize-filter","Authorize filter",[98,2164],"You can authorize certain filters to be active for specific users: public function filters(RestifyRequest $request): array\n{\n    return [\n        ReadyPostsFilter::new()->canSee(\n            fn($request) => $request->user()->isAdmin()\n        ),\n    ];\n}",{"id":3008,"title":3009,"titles":3010,"content":3011,"level":199},"/docs/search/advanced-filters#apply-advanced-filter","Apply advanced filter",[98,2164],"To apply an advanced filter, the frontend has to send the filters query param with a base64 encoded filter: const filters = btoa(JSON.stringify([\n    {\n        'key': 'ready-posts-filter',\n        'value': null,\n    }\n]))\n\nconst  response = await axios.get(`api/restify/posts?filters=${filters}`); The frontend has to encode into base64 an array of filters. Each filter contains 2 things: key - which is the ke-bab form of the filter class name, or a custom $uriKey defined in the filtervalue - this is optional, and represents the value the advanced filter will as a third argument in the filter method",{"id":3013,"title":3014,"titles":3015,"content":3016,"level":188},"/docs/search/advanced-filters#apply-advanced-filters-via-post-request-version-930","Apply advanced filters via POST Request (Version 9.3.0+)",[98],"Starting from version 9.3.0, Laravel Restify introduces the ability to apply advanced filters using a POST request. This enhancement simplifies the process of sending complex filter payloads without the need for base64 encoding. Now, you can send the filters directly as JSON in the request body: const filters = [\n    {\n        'key': 'ready-posts-filter',\n        'value': null,\n    }\n];\n\nconst  response = await axios.post(`api/restify/posts/apply-restify-advanced-filters`, { filters });",{"id":3018,"title":2736,"titles":3019,"content":3020,"level":199},"/docs/search/advanced-filters#custom-uri-key",[98,3014],"Since your class names could change along the way, you can define a $uriKey property to your filters, so the frontend will use always the same key when applying a filter: class ReadyPostsFilter extends AdvancedFilter \n{\n    public static $uriKey = 'ready-posts';\n\n    //...\n\n};",{"id":3022,"title":3023,"titles":3024,"content":3025,"level":199},"/docs/search/advanced-filters#custom-title","Custom title",[98,3014],"class ReadyPostsFilter extends AdvancedFilter \n{\n    public static $title = 'Ready to publish posts';\n\n    //...\n\n};",{"id":3027,"title":3028,"titles":3029,"content":3030,"level":199},"/docs/search/advanced-filters#custom-description","Custom description",[98,3014],"class ReadyPostsFilter extends AdvancedFilter \n{\n    public static $description = 'Filter all posts that are ready to publish';\n\n    //...\n\n};",{"id":3032,"title":3033,"titles":3034,"content":3035,"level":199},"/docs/search/advanced-filters#custom-meta","Custom meta",[98,3014],"class ReadyPostsFilter extends AdvancedFilter \n{\n   public function meta(): array\n   {\n      return [\n          'icon' => 'icon',\n          'color' => 'red',\n          'operators' => [\n              'like' => 'Like', \n              'eq' => 'Equal', \n          ]\n      ];\n   } \n}; Meta will be rendered key/value in the frontend: {\n    ...\n    \"icon\": \"icon\",\n    \"color\": \"red\",\n    \"operators\": {\n        \"like\": \"Like\",\n        \"eq\": \"Equal\"\n    }\n}",{"id":3037,"title":3038,"titles":3039,"content":3040,"level":199},"/docs/search/advanced-filters#advanced-filter-value","Advanced filter value",[98,3014],"The third argument of the filter method is the raw value send by the frontend. Sometimes it might be an array, so you have to get the value using array access: $value['activation']['active'] To avoid this, there is an input method defined into the parent class, so you can use: public function filter(RestifyRequest $request, Relation|Builder $query, $value)\n{\n    $value = $this->input('activation.active', false);\n} This method gets a default value as a second parameter in case the frontend didn't define it.",{"id":3042,"title":3043,"titles":3044,"content":3045,"level":199},"/docs/search/advanced-filters#advanced-filter-rules","Advanced filter rules",[98,3014],"The rules method return an associative array with laravel rules for the payload the frontend should send in the value property for this specific filter. The payload is validated right before it gets to the filter method: public function rules(Request $request): array\n{\n    return [\n        'created_at' => ['required'],\n    ];\n} So the frontend should send the created_at value: {\n    'key': 'ready-posts-filter',\n    'value': { created_at: '2021-01-01' }\n} And you can get this value into the filter method using the advanced filter value: $value = $this->input('created_at', now());",{"id":3047,"title":3048,"titles":3049,"content":3050,"level":188},"/docs/search/advanced-filters#variations","Variations",[98],"Restify ships a few types of build in filter classes you can extend for specific needs.",{"id":3052,"title":3053,"titles":3054,"content":3055,"level":199},"/docs/search/advanced-filters#date-filters","Date filters",[98,3048],"Defining the filter: use Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Binaryk\\LaravelRestify\\Filters\\TimestampFilter;\n\nclass CreatedAfterDateFilter extends TimestampFilter\n{\n    public function filter(RestifyRequest $request, $query, $value)\n    {\n        $query->whereDate('created_at', '>', $value);\n    }\n} Using filter: public function filters(RestifyRequest $request)\n{\n    return [\n        CreatedAfterDateFilter::new(),\n    ];\n} JavaScript implementation: const filters = btoa(JSON.stringify([\n    {\n        'key': 'created-after-date-filter',\n        'value': moment()->timestamp\n    }\n]))\n\nconst  response = await axios.get('api/restify/posts?filters=' + filters);",{"id":3057,"title":3058,"titles":3059,"content":3060,"level":199},"/docs/search/advanced-filters#select-filters","Select Filters",[98,3048],"Defining the filter: \u003C?php\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Binaryk\\LaravelRestify\\Filters\\SelectFilter;\nuse Illuminate\\Http\\Request;\n\nclass SelectCategoryFilter extends SelectFilter\n{\n    public function filter(RestifyRequest $request, $query, $value)\n    {\n        // $value could be 'movie' or 'article'\n        $query->where('category', $value);\n    }\n\n    public function options(Request $request)\n    {\n        return [\n            'Movie category' => 'movie',\n\n            'Article Category' => 'article',\n        ];\n    }\n} Using filter: public function filters(RestifyRequest $request)\n{\n    return [\n        SelectCategoryFilter::new(),\n    ];\n} JavaScript implementation: const filters = btoa(JSON.stringify([\n    {\n        'key': 'select-category-filter',\n        'value': 'article'\n    }\n]))\n\nconst  response = await axios.get('api/restify/posts?filters=' + filters);",{"id":3062,"title":3063,"titles":3064,"content":3065,"level":199},"/docs/search/advanced-filters#boolean-filter","Boolean filter",[98,3048],"Defining the filter: \u003C?php\nuse Binaryk\\LaravelRestify\\BooleanFilter;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\nuse Illuminate\\Http\\Request;\n\nclass ActiveBooleanFilter extends BooleanFilter\n{\n    public function filter(RestifyRequest $request, $query, $value)\n    {\n        $query->where('is_active', $value['is_active']);\n    }\n\n    public function options(Request $request)\n    {\n        return [\n            'Is Active' => 'is_active',\n        ];\n    }\n} Using filter: public function filters(RestifyRequest $request)\n{\n    return [\n        ActiveBooleanFilter::new(),\n    ]; JavaScript implementation: const filters = btoa(JSON.stringify([\n    {\n        'key': 'active-boolean-filter',\n        'value': {\n            'is_active': true,\n        }\n    }\n]))\n\nconst  response = await axios.get('api/restify/posts?filters=' + filters);",{"id":3067,"title":3068,"titles":3069,"content":3070,"level":188},"/docs/search/advanced-filters#multiple-filters","Multiple filters",[98],"You can combine filters as you prefer: const filters = btoa(JSON.stringify([\n    {\n        'key': 'active-boolean-filter',\n        'value': {\n            'is_active': true,\n        }\n    }, \n    {\n        'key': 'select-category-filter',\n        'value': 'article'\n    },\n]))\n\nconst  response = await axios.get('api/restify/posts?filters=' + filters);",{"id":3072,"title":3073,"titles":3074,"content":3075,"level":188},"/docs/search/advanced-filters#get-available-filters","Get available filters",[98],"await axios.get('resitfy-api/posts/filters'); The response will look like this: {\n  \"data\": [\n    {\n      \"key\": \"active-boolean-filter\",\n      \"type\": \"boolean\",\n      \"options\": [\n        {\n          \"label\": \"Is Active\",\n          \"property\": \"is_active\"\n        }\n      ]\n    },\n    {\n      \"key\": \"select-category-filter\",\n      \"type\": \"select\",\n      \"options\": [\n        {\n          \"label\": \"Movie category\",\n          \"property\": \"movie\"\n        },\n        {\n          \"label\": \"Article Category\",\n          \"property\": \"article\"\n        }\n      ]\n    },\n    {\n      \"key\": \"created-after-date-filter\",\n      \"type\": \"timestamp\",\n      \"options\": []\n    },\n    {\n      \"key\": \"email\",\n      \"type\": \"value\",\n      \"description\": \"Email\",\n      \"label\": \"Email\",\n      \"meta\": {\n        \"operator\": \"like\"\n      }\n    }\n  ]\n} Along with custom filters, you can also include in the response the primary filters (as matches), by using ?include query param: /api/restify/posts/filters?include=matches,searchables,sortables",{"id":3077,"title":3078,"titles":3079,"content":3080,"level":188},"/docs/search/advanced-filters#handling-additional-payload-data-in-advanced-filters","Handling Additional Payload Data in Advanced Filters",[98],"In some scenarios, you might want to send additional data beyond the standard key and value in your filter payload. For instance, you may need to specify an operator or a column to apply more complex filtering logic. Laravel Restify Advanced Filters provide a way to handle these additional payload fields using the $this->rest() method. Example Payload Consider the following payload: const filters = [\n    {\n        'key': ValueFilter::uriKey(),\n        'value': 'Valid%',\n        'operator' => 'like',\n        'column' => 'description',\n    }\n];\n\nconst response = await axios.post(`api/restify/posts/apply-restify-advanced-filters`, {filters}); In this payload, besides the standard key and value, we are also sending operator and column. The operator specifies the type of SQL operation, and the column specifies the database column to filter. Using $this->rest() to Access Additional Data To handle these additional fields in your filter class, you need to ensure they are accessible via the $this->rest() method. Here is how you can achieve that: class ValueFilter extends AdvancedFilter\n{\n    public function filter(RestifyRequest $request, Builder|Relation $query, $value)\n    {\n        $operator = $this->rest('operator');\n        $column = $this->rest('column');\n\n        $query->where($column, $operator, $value);\n    }\n\n    public function rules(Request $request): array\n    {\n        return [];\n    }\n} html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":103,"title":102,"titles":3082,"content":3083,"level":182},[],"Learn how to sort API results in Laravel Restify using the $sort property, query parameters, and relation-based sorting for hasOne relationships.",{"id":3085,"title":2164,"titles":3086,"content":3087,"level":188},"/docs/search/sorting#definition",[102],"During index requests, usually we have to sort by specific attributes. This requires the $sort configuration: class PostRepository extends Repository\n{\n    public static array $sort = ['id']; Performing request requires the sort query param:",{"id":3089,"title":3090,"titles":3091,"content":3092,"level":188},"/docs/search/sorting#descending-sorting","Descending sorting",[102],"Sorting DESC requires a minus (-) sign before the attribute name: GET: /api/restify/posts?sort=-id Sorting ASC: GET: /api/restify/posts?sort=id or with plus sign before the field: GET: /api/restify/posts?sort=+id",{"id":3094,"title":3095,"titles":3096,"content":3097,"level":188},"/docs/search/sorting#sort-using-relation","Sort using relation",[102],"Sometimes you may need to sort by a belongsTo or hasOne relationship. This become a breeze with Restify. Firstly you have to instruct your sort to use a relationship:",{"id":3099,"title":3100,"titles":3101,"content":3102,"level":199},"/docs/search/sorting#hasone-sorting","HasOne sorting",[102,3095],"Using a related relationship, it becomes very easy to define a sortable by has one related. You simply add the ->sortable() method to the relationship: public function related(): array\n{\n    return [\n        'post' => HasOne::make('post', PostRepository::class)->sortable('title'),\n    ];\n} The sortable method accepts the column (or fully qualified column name) of the related model. The API request will always have to use the full path to the attributes: GET: /api/restify/posts?sort=post.attributes.title The structure of the sort query param value consist always from 3 parts: post - the name of the relation defined in the related methodattributes - a generic json:api termtitle - the column name from the database of the related model",{"id":3104,"title":3105,"titles":3106,"content":3107,"level":199},"/docs/search/sorting#belongsto-sorting","BelongsTo sorting",[102,3095],"The belongsTo sorting works in a similar way. You simply add the ->sortable() method to the relationship: public function related(): array\n{\n    return [\n        'user' => BelongsTo::make('user', UserRepository::class)->sortable('name'),\n    ];\n}",{"id":3109,"title":3110,"titles":3111,"content":3112,"level":199},"/docs/search/sorting#using-custom-sortable-filter","Using custom sortable filter",[102,3095],"You can override the sorts method, and return an instance of SortableFilter that might be instructed to use a relationship: // PostRepository\nuse Binaryk\\LaravelRestify\\Fields\\BelongsTo;\nuse Binaryk\\LaravelRestify\\Filters\\SortableFilter;\n\npublic static function sorts(): array\n{\n    return [\n        'users.name' => SortableFilter::make()\n            ->setColumn('users.name')\n            ->usingRelation(\n                BelongsTo::make('user', 'user', UserRepository::class),\n        )\n    ];\n} Make sure that the column is fully qualified (include the table name). The request could look like: GET: /api/restify/posts?sort=-users.name This will return all posts, sorted descending by users name. As you may notice we have typed twice the users.name (on the array key, and as argument in the setColumn method). As soon as you use the fully qualified key name, you can avoid the setColumn call, since the column will be injected automatically based on the sorts key.",{"id":3114,"title":3115,"titles":3116,"content":3117,"level":188},"/docs/search/sorting#join-strategy-for-sort-by-relation","JOIN strategy for sort-by-relation",[102],"By default, sorting by a BelongsTo or HasOne relation column emits a correlated subquery in the ORDER BY clause: ORDER BY (SELECT vendors.code FROM vendors WHERE vendors.id = invoices.vendor_id LIMIT 1) ASC This evaluates per row and prevents the database optimizer from using indexes on the related column. Restify can emit a LEFT JOIN instead, which is index-friendly: LEFT JOIN vendors ON invoices.vendor_id = vendors.id\nORDER BY vendors.code ASC",{"id":3119,"title":3120,"titles":3121,"content":3122,"level":199},"/docs/search/sorting#enable-globally","Enable globally",[102,3115],"Flip the config flag (default false) to switch every sort-by-relation to LEFT JOIN: 'sort' => [\n    'use_joins_for_belongs_to' => env('RESTIFY_SORT_USE_JOINS_FOR_BELONGS_TO', false),\n],",{"id":3124,"title":3125,"titles":3126,"content":3127,"level":199},"/docs/search/sorting#override-per-filter","Override per filter",[102,3115],"Force LEFT JOIN (or the legacy subquery) for a single sortable, regardless of the config flag: use Binaryk\\LaravelRestify\\Fields\\BelongsTo;\nuse Binaryk\\LaravelRestify\\Filters\\SortableFilter;\n\npublic static function sorts(): array\n{\n    return [\n        'users.name' => SortableFilter::make()\n            ->setColumn('users.name')\n            ->usingRelation(BelongsTo::make('user', UserRepository::class))\n            ->useJoin(),       // force LEFT JOIN\n            // ->useSubquery() // force legacy subquery\n    ];\n} Resolution order: per-filter (useJoin / useSubquery) wins over the config flag, which wins over the legacy subquery default. When search has already left-joined the same related table (because BelongsTo->searchable([...]) is configured and restify.search.use_joins_for_belongs_to=true), the sort path detects the existing join and reuses it — exactly one join per related table. LEFT JOIN is supported on BelongsTo and HasOne only — the two relation types usingRelation() accepts. HasMany, MorphTo, and BelongsToMany always use the subquery path; a LEFT JOIN would multiply rows and corrupt the result set.",{"id":3129,"title":3130,"titles":3131,"content":3132,"level":188},"/docs/search/sorting#sort-using-closure","Sort using closure",[102],"If you have a quick sort method, you can use a closure to sort your data: // PostRepository\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\n\npublic static function sorts(): array\n{\n    return [\n        'users.name' => function(RestifyRequest $request, $query, $direction) {\n            // custom sort\n        }\n    ];\n}",{"id":3134,"title":3135,"titles":3136,"content":3137,"level":188},"/docs/search/sorting#invokable-custom-sort-classes","Invokable Custom Sort Classes",[102],"Alongside the already provided sorting mechanisms, you can also use an invokable class. This provides you with a flexible way to create your own custom sort logic using classes.",{"id":3139,"title":1529,"titles":3140,"content":3141,"level":199},"/docs/search/sorting#basic-usage",[102,3135],"Such classes should implement the __invoke method, which will be called during sorting. Here's an example: class NaturalSort {\n    public function __invoke(RestifyRequest $request, Builder $query, string $order, string $column): void\n    {\n        $query->orderBy($column, $order);\n    }\n}; You can then use it in your Repository's sorts method like this: public static function sorts(): array\n{\n    return [\n        'name' => app(NaturalSort::class),\n    ];\n} However, for even more convenience, you can also directly specify the invokable class name as a string: PostRepository::$sort = [\n    'name' => NaturalSort::class,\n];",{"id":3143,"title":3144,"titles":3145,"content":3146,"level":199},"/docs/search/sorting#built-in-natural-sort-filter","Built-in Natural Sort Filter",[102,3135],"For those not looking to write their own sort filters, binaryk/laravel-restify already provides a built-in natural sort filter. This allows you to quickly sort fields in a natural order without additional implementations: use Binaryk\\LaravelRestify\\Filters\\Sorts\\NaturalSortFilter;\n\nPostRepository::$sort = [\n    'name' => NaturalSortFilter::class,\n]; Using the NaturalSortFilter class, you can effortlessly apply natural sorting to your repository fields.",{"id":3148,"title":3149,"titles":3150,"content":3151,"level":188},"/docs/search/sorting#get-available-sorts","Get available sorts",[102],"You can use the following request to get sortable attributes for a repository: /api/restify/posts/filters?only=sortables To get all filters, you can use /api/restify/posts/filters?only=sortables,matches,searchables. html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":112,"title":111,"titles":3153,"content":3154,"level":182},[],"Laravel Restify auto-generates GraphQL schemas and resolvers from your repositories, enabling full CRUD operations via GraphQL using Lighthouse. Laravel Restify provides powerful GraphQL integration, allowing you to automatically generate GraphQL schemas and resolvers from your existing Restify repositories. This enables you to quickly add GraphQL capabilities to your API without rewriting your business logic.",{"id":3156,"title":1263,"titles":3157,"content":3158,"level":188},"/docs/graphql/graphql#overview",[111],"The GraphQL integration in Laravel Restify provides: Automatic Schema Generation - Convert your Restify repositories into GraphQL type definitionsResolver Generation - Create PHP resolver classes for handling GraphQL operationsField Mapping - Intelligent mapping from Restify fields to GraphQL typesCRUD Operations - Full Create, Read, Update, Delete operations via GraphQLAuthentication Context - Proper handling of authentication in console/generation contextPreview Mode - See what will be generated before creating files",{"id":3160,"title":18,"titles":3161,"content":224,"level":188},"/docs/graphql/graphql#quick-start",[111],{"id":3163,"title":3164,"titles":3165,"content":3166,"level":199},"/docs/graphql/graphql#_1-install-dependencies","1. Install Dependencies",[111,18],"First, install the required GraphQL packages: composer require pusher/pusher-php-server lighthouse/lighthouse",{"id":3168,"title":3169,"titles":3170,"content":3171,"level":199},"/docs/graphql/graphql#_2-generate-your-schema","2. Generate Your Schema",[111,18],"Use the Artisan command to generate GraphQL schema from your repositories: php artisan restify:graphql:generate --resolvers This command will: Analyze your Restify repositoriesGenerate GraphQL type definitionsCreate resolver classes (if --resolvers flag is used)Show a preview before generating files",{"id":3173,"title":3174,"titles":3175,"content":3176,"level":199},"/docs/graphql/graphql#_3-configure-lighthouse","3. Configure Lighthouse",[111,18],"Update your config/lighthouse.php to use the generated schema: 'schema' => [\n    'register' => app_path('GraphQL/schema.graphql'),\n],",{"id":3178,"title":3179,"titles":3180,"content":3181,"level":199},"/docs/graphql/graphql#_4-add-graphql-endpoint","4. Add GraphQL Endpoint",[111,18],"Add the GraphQL route to your application: Route::restifyGraphQL();",{"id":3183,"title":3184,"titles":3185,"content":3186,"level":188},"/docs/graphql/graphql#example-generated-schema","Example Generated Schema",[111],"For a UserRepository with fields like name, email, and created_at, the generator creates: type User {\n    id: ID!\n    name: String\n    email: String\n    created_at: String\n    updated_at: String\n}\n\ninput UserInput {\n    name: String\n    email: String\n}\n\ntype Query {\n    user(id: ID!): User\n    userList(first: Int = 15, page: Int = 1): [User!]!\n}\n\ntype Mutation {\n    createUser(input: UserInput!): User!\n    updateUser(id: ID!, input: UserInput!): User!\n    deleteUser(id: ID!): Boolean!\n}",{"id":3188,"title":404,"titles":3189,"content":224,"level":188},"/docs/graphql/graphql#benefits",[111],{"id":3191,"title":3192,"titles":3193,"content":3194,"level":199},"/docs/graphql/graphql#rapid-development","Rapid Development",[111,404],"Generate complete GraphQL APIs from existing Restify repositoriesNo need to manually write type definitions or resolversMaintain consistency between REST and GraphQL APIs",{"id":3196,"title":3197,"titles":3198,"content":3199,"level":199},"/docs/graphql/graphql#automatic-type-safety","Automatic Type Safety",[111,404],"Field types are automatically mapped to appropriate GraphQL typesInput validation leverages existing Restify field definitionsType safety maintained throughout the stack",{"id":3201,"title":3202,"titles":3203,"content":3204,"level":199},"/docs/graphql/graphql#seamless-integration","Seamless Integration",[111,404],"Uses existing Restify authorization and field visibility rulesLeverages repository business logic and relationshipsMaintains consistency with REST API behavior",{"id":3206,"title":3207,"titles":3208,"content":224,"level":188},"/docs/graphql/graphql#use-cases","Use Cases",[111],{"id":3210,"title":3211,"titles":3212,"content":3213,"level":199},"/docs/graphql/graphql#api-unification","API Unification",[111,3207],"Convert your REST API to also support GraphQL without duplicating logic: # Generate GraphQL from existing repositories\nphp artisan restify:graphql:generate --resolvers --force",{"id":3215,"title":3216,"titles":3217,"content":3218,"level":199},"/docs/graphql/graphql#mobile-app-backend","Mobile App Backend",[111,3207],"Provide GraphQL for mobile apps while maintaining REST for web clients: # Generate to mobile-specific directory\nphp artisan restify:graphql:generate --output-path=app/GraphQL/Mobile --resolvers",{"id":3220,"title":3221,"titles":3222,"content":3223,"level":199},"/docs/graphql/graphql#third-party-integrations","Third-Party Integrations",[111,3207],"Create GraphQL schemas for external services that prefer GraphQL: # Generate with custom schema naming\nphp artisan restify:graphql:generate --schema-file=external-api.graphql",{"id":3225,"title":267,"titles":3226,"content":3227,"level":188},"/docs/graphql/graphql#next-steps",[111],"Schema Generation - Learn about the schema generation command and its optionsLighthouse Documentation - Explore advanced GraphQL featuresGraphQL Playground - Test your GraphQL API interactively",{"id":3229,"title":379,"titles":3230,"content":3231,"level":188},"/docs/graphql/graphql#configuration",[111],"The GraphQL generation process can be customized through the configuration file. Publish it using: php artisan vendor:publish --tag=restify-graphql-config This allows you to customize: Field type mappingsRepository filteringOutput formatting preferencesSchema generation options html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":116,"title":115,"titles":3233,"content":3234,"level":182},[],"Learn how Laravel Restify auto-generates GraphQL schemas, queries, mutations, and resolver classes from your repositories using a single Artisan command. Laravel Restify can automatically generate GraphQL schemas and resolvers from your existing Restify repositories, allowing you to quickly add GraphQL capabilities to your API.",{"id":3236,"title":1263,"titles":3237,"content":3238,"level":188},"/docs/graphql/graphql-generation#overview",[115],"The GraphQL generation feature analyzes your Restify repositories and creates: GraphQL Type Definitions - Based on your repository fieldsQuery Operations - For fetching individual resources and collectionsMutation Operations - For creating, updating, and deleting resourcesInput Types - For mutation operationsResolver Classes - PHP classes that handle GraphQL operations (optional)",{"id":3240,"title":191,"titles":3241,"content":224,"level":188},"/docs/graphql/graphql-generation#installation",[115],{"id":3243,"title":3244,"titles":3245,"content":3246,"level":199},"/docs/graphql/graphql-generation#_1-install-lighthouse-graphql","1. Install Lighthouse GraphQL",[115,191],"First, install the Lighthouse GraphQL package: composer require pusher/pusher-php-server lighthouse/lighthouse",{"id":3248,"title":3249,"titles":3250,"content":3251,"level":199},"/docs/graphql/graphql-generation#_2-publish-lighthouse-configuration","2. Publish Lighthouse Configuration",[115,191],"php artisan vendor:publish --tag=lighthouse-config",{"id":3253,"title":1529,"titles":3254,"content":224,"level":188},"/docs/graphql/graphql-generation#basic-usage",[115],{"id":3256,"title":3257,"titles":3258,"content":3259,"level":199},"/docs/graphql/graphql-generation#generate-graphql-schema","Generate GraphQL Schema",[115,1529],"The simplest way to generate a GraphQL schema from your repositories: php artisan restify:graphql:generate This command will: Analyze all registered Restify repositoriesShow a preview of what will be generatedAsk for confirmation before proceedingGenerate a GraphQL schema file at app/GraphQL/schema.graphql",{"id":3261,"title":3262,"titles":3263,"content":3264,"level":199},"/docs/graphql/graphql-generation#generate-with-resolvers","Generate with Resolvers",[115,1529],"To also generate PHP resolver classes: php artisan restify:graphql:generate --resolvers This creates resolver classes in app/GraphQL/Resolvers/ that handle the GraphQL operations.",{"id":3266,"title":1539,"titles":3267,"content":224,"level":188},"/docs/graphql/graphql-generation#command-options",[115],{"id":3269,"title":3270,"titles":3271,"content":3272,"level":199},"/docs/graphql/graphql-generation#basic-options","Basic Options",[115,1539],"OptionDescription--forceOverwrite existing files without prompting--skip-previewSkip the preview and generate files immediately--resolversGenerate PHP resolver classes",{"id":3274,"title":3275,"titles":3276,"content":3277,"level":199},"/docs/graphql/graphql-generation#output-configuration","Output Configuration",[115,1539],"OptionDescriptionDefault--output-pathDirectory for generated filesapp/GraphQL--schema-fileName of the schema fileschema.graphql",{"id":3279,"title":1583,"titles":3280,"content":3281,"level":199},"/docs/graphql/graphql-generation#examples",[115,1539],"# Generate to custom directory\nphp artisan restify:graphql:generate --output-path=resources/graphql\n\n# Use custom schema filename\nphp artisan restify:graphql:generate --schema-file=api-schema.graphql\n\n# Generate everything without prompts\nphp artisan restify:graphql:generate --resolvers --force --skip-preview",{"id":3283,"title":3284,"titles":3285,"content":224,"level":188},"/docs/graphql/graphql-generation#generated-schema-structure","Generated Schema Structure",[115],{"id":3287,"title":3288,"titles":3289,"content":3290,"level":199},"/docs/graphql/graphql-generation#type-definitions","Type Definitions",[115,3284],"For a UserRepository, the command generates: type User {\n    id: ID!\n    name: String\n    email: String\n    created_at: String\n    updated_at: String\n}\n\ninput UserInput {\n    name: String\n    email: String\n}",{"id":3292,"title":3293,"titles":3294,"content":3295,"level":199},"/docs/graphql/graphql-generation#query-operations","Query Operations",[115,3284],"type Query {\n    user(id: ID!): User\n    userList(first: Int = 15, page: Int = 1): [User!]!\n}",{"id":3297,"title":3298,"titles":3299,"content":3300,"level":199},"/docs/graphql/graphql-generation#mutation-operations","Mutation Operations",[115,3284],"type Mutation {\n    createUser(input: UserInput!): User!\n    updateUser(id: ID!, input: UserInput!): User!\n    deleteUser(id: ID!): Boolean!\n}",{"id":3302,"title":3303,"titles":3304,"content":3305,"level":188},"/docs/graphql/graphql-generation#field-type-mapping","Field Type Mapping",[115],"Laravel Restify fields are automatically mapped to GraphQL types: Restify FieldGraphQL TypeField, Text, TextareaStringNumber, IntegerIntFloat, DecimalFloatBoolean, ToggleBooleanDate, DateTimeStringJsonJSONMultiSelect[String!]BelongsToID (input) / String (output)HasMany[ID!] (input) / [String!] (output)",{"id":3307,"title":3308,"titles":3309,"content":3310,"level":188},"/docs/graphql/graphql-generation#generated-resolvers","Generated Resolvers",[115],"When using --resolvers, the command creates resolver classes with standard CRUD operations: \u003C?php\n\nnamespace App\\GraphQL\\Resolvers;\n\nuse App\\Restify\\UserRepository;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\RestifyRequest;\n\nclass UserResolver\n{\n    public function show($root, array $args, $context, $info)\n    {\n        $repository = new UserRepository();\n        $model = $repository::newModel()->findOrFail($args['id']);\n\n        return $repository->setModel($model)\n            ->serializeForShow(RestifyRequest::createFrom(request()));\n    }\n\n    public function index($root, array $args, $context, $info)\n    {\n        $repository = new UserRepository();\n        $query = $repository::newModel()->query();\n\n        $page = $args['page'] ?? 1;\n        $perPage = $args['first'] ?? 15;\n\n        return $query->paginate($perPage, ['*'], 'page', $page)->items();\n    }\n\n    public function create($root, array $args, $context, $info)\n    {\n        $repository = new UserRepository();\n        $request = RestifyRequest::createFrom(request());\n        $request->merge($args['input']);\n\n        return $repository->store($request);\n    }\n\n    public function update($root, array $args, $context, $info)\n    {\n        $repository = new UserRepository();\n        $request = RestifyRequest::createFrom(request());\n        $request->merge($args['input']);\n\n        return $repository->update($request, $args['id']);\n    }\n\n    public function delete($root, array $args, $context, $info)\n    {\n        $repository = new UserRepository();\n        $request = RestifyRequest::createFrom(request());\n\n        $repository->destroy($request, $args['id']);\n\n        return true;\n    }\n}",{"id":3312,"title":379,"titles":3313,"content":3314,"level":188},"/docs/graphql/graphql-generation#configuration",[115],"The generation process can be customized using the configuration file: php artisan vendor:publish --tag=restify-graphql-config This publishes config/restify-graphql.php where you can configure: Field type mappingsSchema generation optionsRepository filteringOutput formatting preferences",{"id":3316,"title":3317,"titles":3318,"content":3319,"level":188},"/docs/graphql/graphql-generation#preview-mode","Preview Mode",[115],"By default, the command shows a detailed preview before generating files: 📋 Preview of files to be generated:\n═══════════════════════════════════════════════════════\n\n🔍 Found 3 repositories:\n   • UserRepository\n   • PostRepository\n   • CompanyRepository\n\n📂 Output configuration:\n   Output directory: app/GraphQL\n   Schema file: schema.graphql\n   Generate resolvers: Yes\n   Force overwrite: No\n\n📄 Files that will be generated:\n   1. app/GraphQL/schema.graphql\n   2. Resolvers directory: app/GraphQL/Resolvers/\n      3. app/GraphQL/Resolvers/UserResolver.php\n      4. app/GraphQL/Resolvers/PostResolver.php\n      5. app/GraphQL/Resolvers/CompanyResolver.php\n\n📝 Sample GraphQL schema preview:\n   ┌─────────────────────────────────────────────────────┐\n   │ type User {                                         │\n   │   id: ID!                                           │\n   │   name: String                                      │\n   │   email: String                                     │\n   │   created_at: String                                │\n   │   updated_at: String                                │\n   │ }                                                   │\n   │                                                     │\n   │ type Query {                                        │\n   │   user(id: ID!): User                              │\n   │   userList(first: Int, page: Int): [User!]!        │\n   │ }                                                   │\n   │                                                     │\n   │ type Mutation {                                     │\n   │   createUser(input: UserInput!): User!             │\n   │   updateUser(id: ID!, input: UserInput!): User!    │\n   │   deleteUser(id: ID!): Boolean!                     │\n   │ }                                                   │\n   │                                                     │\n   │ # ... plus 2 more types                            │\n   └─────────────────────────────────────────────────────┘",{"id":3321,"title":3322,"titles":3323,"content":3324,"level":188},"/docs/graphql/graphql-generation#lighthouse-integration","Lighthouse Integration",[115],"After generating your schema and resolvers, configure Lighthouse to use them:",{"id":3326,"title":3327,"titles":3328,"content":3329,"level":199},"/docs/graphql/graphql-generation#_1-update-lighthouse-config","1. Update Lighthouse Config",[115,3322],"Edit config/lighthouse.php: 'schema' => [\n    'register' => app_path('GraphQL/schema.graphql'),\n],",{"id":3331,"title":3332,"titles":3333,"content":3334,"level":199},"/docs/graphql/graphql-generation#_2-register-resolvers","2. Register Resolvers",[115,3322],"If you generated resolver classes, register them in your GraphQL schema by adding directives: type Query {\n    user(id: ID!): User @field(resolver: \"App\\\\GraphQL\\\\Resolvers\\\\UserResolver@show\")\n    userList(first: Int = 15, page: Int = 1): [User!]! @field(resolver: \"App\\\\GraphQL\\\\Resolvers\\\\UserResolver@index\")\n}\n\ntype Mutation {\n    createUser(input: UserInput!): User! @field(resolver: \"App\\\\GraphQL\\\\Resolvers\\\\UserResolver@create\")\n    updateUser(id: ID!, input: UserInput!): User! @field(resolver: \"App\\\\GraphQL\\\\Resolvers\\\\UserResolver@update\")\n    deleteUser(id: ID!): Boolean! @field(resolver: \"App\\\\GraphQL\\\\Resolvers\\\\UserResolver@delete\")\n}",{"id":3336,"title":3337,"titles":3338,"content":3339,"level":199},"/docs/graphql/graphql-generation#_3-add-graphql-route","3. Add GraphQL Route",[115,3322],"Add the GraphQL endpoint to your routes: Route::middleware(['api'])->group(function () {\n    Route::post('/graphql', \\Nuwave\\Lighthouse\\Http\\GraphQLController::class);\n});",{"id":3341,"title":3342,"titles":3343,"content":3344,"level":188},"/docs/graphql/graphql-generation#authentication-context","Authentication Context",[115],"The GraphQL generation command automatically handles authentication mocking in console context. This ensures that repositories with permission checks (like $request->user()->can()) work properly during schema generation. The mock user created during generation: Always returns true for permission checksProvides basic user properties (id: 1)Handles any authentication-related method calls",{"id":3346,"title":267,"titles":3347,"content":3348,"level":188},"/docs/graphql/graphql-generation#next-steps",[115],"After generating your GraphQL schema: Install Lighthouse if not already installedConfigure Lighthouse to use the generated schemaRegister resolvers in your GraphQL setupTest your GraphQL endpoint using tools like GraphQL PlaygroundCustomize the schema as needed for your specific requirements",{"id":3350,"title":1622,"titles":3351,"content":224,"level":188},"/docs/graphql/graphql-generation#troubleshooting",[115],{"id":3353,"title":3354,"titles":3355,"content":3356,"level":199},"/docs/graphql/graphql-generation#field-collection-issues","Field Collection Issues",[115,1622],"If some fields are missing from the generated schema, ensure your repository's fields() method is properly implemented and doesn't have complex conditional logic that prevents field collection.",{"id":3358,"title":1636,"titles":3359,"content":3360,"level":199},"/docs/graphql/graphql-generation#permission-errors",[115,1622],"The command includes authentication mocking, but if you encounter permission-related errors, check that your repositories handle the mock authentication context properly.",{"id":3362,"title":3363,"titles":3364,"content":3365,"level":199},"/docs/graphql/graphql-generation#custom-field-types","Custom Field Types",[115,1622],"If you have custom field types that aren't mapped correctly, you can extend the type mapping in the configuration file or modify the generated schema manually. html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":125,"title":124,"titles":3367,"content":3368,"level":182},[],"Learn how Laravel Restify integrates with the Model Context Protocol to expose your repositories as tools that AI agents can discover and call. Laravel Restify provides seamless integration with the Model Context Protocol (MCP), allowing AI agents to interact with your REST API resources through structured tool interfaces. So you can simply tranform your repositories into a tools for AI agents to consume. Incredible!",{"id":3370,"title":3371,"titles":3372,"content":224,"level":188},"/docs/mcp/mcp#setup-registration","Setup & Registration",[124],{"id":3374,"title":3375,"titles":3376,"content":3377,"level":199},"/docs/mcp/mcp#basic-server-registration","Basic Server Registration",[124,3371],"Register the MCP server in your application's service provider or routes file: use Binaryk\\LaravelRestify\\MCP\\RestifyServer;\nuse Laravel\\Mcp\\Facades\\Mcp;\n\n// Register the MCP server\nMcp::web('restify', RestifyServer::class)->name('mcp.restify'); This creates an MCP server endpoint at /mcp/restify that AI agents can connect to.",{"id":3379,"title":3380,"titles":3381,"content":3382,"level":199},"/docs/mcp/mcp#adding-authentication-middleware","Adding Authentication & Middleware",[124,3371],"For production applications, you'll want to add authentication and other middleware: use Binaryk\\LaravelRestify\\MCP\\RestifyServer;\nuse Laravel\\Mcp\\Facades\\Mcp;\n\nMcp::web('restify', RestifyServer::class)->middleware([\n    'auth:sanctum',\n])->name('mcp.restify'); And that's it! Now you can access your Restify API through the MCP server with authentication. Go into n8n or your AI agent of choice and connect to the MCP server endpoint.",{"id":3384,"title":3385,"titles":3386,"content":3387,"level":199},"/docs/mcp/mcp#terminalstdin-access-local-mcp","Terminal/STDIN Access (Local MCP)",[124,3371],"For terminal-based AI agents (like Claude Desktop, cursor, or other CLI tools that support MCP), you can expose your Restify API through STDIN/STDOUT using the local syntax. This allows direct integration without HTTP overhead.",{"id":3389,"title":3390,"titles":3391,"content":3392,"level":834},"/docs/mcp/mcp#registering-a-local-mcp-server","Registering a Local MCP Server",[124,3371,3385],"Register your local MCP server in the routes/ai.php file: use Laravel\\Mcp\\Facades\\Mcp;\nuse App\\Mcp\\Servers\\GroweeStdServer;\n\n// Register for terminal/STDIN access\nMcp::local('growee', GroweeStdServer::class);",{"id":3394,"title":3395,"titles":3396,"content":3397,"level":834},"/docs/mcp/mcp#creating-a-terminal-accessible-server-with-authentication","Creating a Terminal-Accessible Server with Authentication",[124,3371,3385],"When using terminal access, authentication must be handled within the server's boot() method since there's no HTTP middleware pipeline. Here's a complete example that extends RestifyServer and implements Sanctum authentication: \u003C?php\n\nnamespace App\\Mcp\\Servers;\n\nuse Binaryk\\LaravelRestify\\MCP\\RestifyServer;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Auth;\nuse Laravel\\Mcp\\Server\\Exceptions\\McpException;\nuse Laravel\\Sanctum\\PersonalAccessToken;\n\nclass GroweeStdServer extends RestifyServer\n{\n    public function boot(): void\n    {\n        $request = request();\n\n        // Get the API token from Authorization header\n        $bearerToken = $request->bearerToken();\n\n        // Fail if no API key is provided\n        if (! $bearerToken) {\n            throw new McpException('API key is required. Please provide a Bearer token in the Authorization header.');\n        }\n\n        // Try to authenticate using Sanctum\n        $token = PersonalAccessToken::findToken($bearerToken);\n\n        if (! $token) {\n            throw new McpException('Invalid API key provided. Please check your Bearer token.');\n        }\n\n        // Verify the token is active\n        if (! $token->tokenable) {\n            throw new McpException('API token is not associated with a valid user.');\n        }\n\n        // Set the authenticated user on both sanctum and default guard\n        Auth::guard('sanctum')->setUser($token->tokenable);\n        Auth::setUser($token->tokenable);\n\n        // Set the user resolver for the request\n        $request->setUserResolver(function () use ($token) {\n            return $token->tokenable;\n        });\n        \n        // Call parent boot to discover tools, resources, and prompts\n        parent::boot();\n    }\n}",{"id":3399,"title":3400,"titles":3401,"content":3402,"level":834},"/docs/mcp/mcp#key-differences-from-web-access","Key Differences from Web Access",[124,3371,3385],"No Middleware Pipeline: Unlike Mcp::web(), the local syntax doesn't support middleware. All authentication and authorization must be implemented in the boot() method.Direct API Token: Terminal clients provide Bearer tokens directly through the Authorization header.Request Context: Access to the request is available via the request() helper function.Error Handling: Use McpException for authentication failures to provide clear error messages to the terminal client.Registration Location: Local MCP servers are typically registered in routes/ai.php instead of web routes.",{"id":3404,"title":3405,"titles":3406,"content":3407,"level":834},"/docs/mcp/mcp#client-configuration","Client Configuration",[124,3371,3385],"Terminal-based AI clients (like Claude Desktop) can connect to your local MCP server by configuring the connection with your API token. The exact configuration depends on your client, but typically involves: Setting the Authorization header with your Sanctum tokenSpecifying the server name (e.g., 'growee')Pointing to your Laravel application's MCP endpoint",{"id":3409,"title":3410,"titles":3411,"content":3412,"level":834},"/docs/mcp/mcp#security-considerations","Security Considerations",[124,3371,3385],"Token Security: API tokens are passed via the Authorization header and should be kept secureToken Scopes: Consider implementing token abilities/scopes to limit accessRate Limiting: Implement rate limiting at the application level since middleware isn't availableAudit Logging: Log authentication attempts and API usage for security monitoringToken Rotation: Implement token expiration and rotation policies",{"id":3414,"title":3415,"titles":3416,"content":3417,"level":834},"/docs/mcp/mcp#security-best-practices","🔒 Security Best Practices",[124,3371,3385],"Apply field visibility controls (hideFromMcp()) for sensitive dataAudit MCP field access patternsImplement rate limiting for token-heavy operations",{"id":3419,"title":3420,"titles":3421,"content":224,"level":188},"/docs/mcp/mcp#common-issues","Common Issues",[124],{"id":3423,"title":3424,"titles":3425,"content":3426,"level":199},"/docs/mcp/mcp#schema-validation-error","Schema Validation Error",[124,3420],"Error: [ERROR: Received tool input did not match expected schema] Cause: This occurs when the field type is not identified correctly by the MCP server, leading to schema mismatches between what the AI agent sends and what Laravel Restify expects. Solution: You need to explicitly override the field type for the MCP schema using the toolSchema() method: field('project_id')\n    ->toolSchema(function(\\Laravel\\Mcp\\Server\\Tools\\ToolInputSchema $schema) {\n        $schema->string('project_id')\n            ->description('The ID of the project associated with the timesheet entry.')\n            ->required();\n    }), This approach allows you to: Explicitly define the expected data type (string, integer, boolean, etc.)Add detailed descriptions for AI agentsSet validation rules (required, optional)Override automatic type inference when it's incorrect",{"id":3428,"title":379,"titles":3429,"content":3430,"level":188},"/docs/mcp/mcp#configuration",[124],"The MCP integration respects your existing Restify configuration and adds MCP-specific options: 'mcp' => [\n    'enabled' => true,\n    'server_name' => 'My App MCP Server',\n    'server_version' => '1.0.0',\n    'default_pagination' => 25,\n    'mode' => env('RESTIFY_MCP_MODE', 'direct'), // 'direct' or 'wrapper'\n    'tools' => [\n        'exclude' => [\n            // Tools to exclude from discovery\n        ],\n        'include' => [\n            // Additional tools to include\n        ],\n    ],\n],",{"id":3432,"title":3433,"titles":3434,"content":3435,"level":188},"/docs/mcp/mcp#mcp-mode-direct-vs-wrapper","MCP Mode: Direct vs Wrapper",[124],"Laravel Restify offers two modes for exposing your repositories through MCP: Direct Mode and Wrapper Mode. Each mode has different trade-offs in terms of token usage and discoverability.",{"id":3437,"title":3438,"titles":3439,"content":3440,"level":199},"/docs/mcp/mcp#direct-mode-default","Direct Mode (Default)",[124,3433],"In direct mode, every repository operation (index, show, store, update, delete) and custom action/getter is exposed as a separate MCP tool. This provides maximum discoverability for AI agents. When to use Direct Mode: You have a small number of repositories (\u003C 10)You want AI agents to instantly see all available operationsToken usage is not a concernYou prefer simpler, more straightforward tool discovery Example: With 10 repositories, each having 5 CRUD operations plus 2 actions, you would expose 70 tools to the AI agent. Configuration: // .env\nRESTIFY_MCP_MODE=direct",{"id":3442,"title":3443,"titles":3444,"content":3445,"level":199},"/docs/mcp/mcp#wrapper-mode-token-efficient","Wrapper Mode (Token-Efficient)",[124,3433],"Wrapper mode uses a progressive discovery pattern that exposes only 4 wrapper tools regardless of how many repositories you have. AI agents discover and execute operations through a multi-step workflow. When to use Wrapper Mode: You have many repositories (10+)Token usage efficiency is important (e.g., working with large context windows)You want to reduce the initial tool list sizeYou're building complex applications with dozens of repositories Token Savings Example: Direct mode with 50 repositories: ~250+ tools exposedWrapper mode with 50 repositories: 4 tools exposedToken reduction: ~98% fewer tokens used for tool definitions Configuration: // .env\nRESTIFY_MCP_MODE=wrapper",{"id":3447,"title":3448,"titles":3449,"content":3450,"level":199},"/docs/mcp/mcp#the-4-wrapper-tools","The 4 Wrapper Tools",[124,3433],"When using wrapper mode, AI agents use these 4 tools in a progressive discovery workflow:",{"id":3452,"title":3453,"titles":3454,"content":3455,"level":834},"/docs/mcp/mcp#_1-discover-repositories","1. discover-repositories",[124,3433,3448],"Lists all available MCP-enabled repositories with metadata. Supports optional search filtering. Example Request: {\n  \"search\": \"user\"\n} Example Response: {\n  \"success\": true,\n  \"repositories\": [\n    {\n      \"name\": \"users\",\n      \"title\": \"Users\",\n      \"description\": \"Manage user accounts\",\n      \"operations\": [\"index\", \"show\", \"store\", \"update\", \"delete\", \"profile\"],\n      \"actions_count\": 2,\n      \"getters_count\": 1\n    }\n  ]\n}",{"id":3457,"title":3458,"titles":3459,"content":3460,"level":834},"/docs/mcp/mcp#_2-get-repository-operations","2. get-repository-operations",[124,3433,3448],"Lists all operations, actions, and getters available for a specific repository. Example Request: {\n  \"repository\": \"users\"\n} Example Response: {\n  \"success\": true,\n  \"repository\": \"users\",\n  \"crud_operations\": [\"index\", \"show\", \"store\", \"update\", \"delete\", \"profile\"],\n  \"actions\": [\n    {\n      \"name\": \"activate-user\",\n      \"title\": \"Activate User\",\n      \"description\": \"Activate a user account\"\n    }\n  ],\n  \"getters\": [\n    {\n      \"name\": \"active-users\",\n      \"title\": \"Active Users\",\n      \"description\": \"Get all active users\"\n    }\n  ]\n}",{"id":3462,"title":3463,"titles":3464,"content":3465,"level":834},"/docs/mcp/mcp#_3-get-operation-details","3. get-operation-details",[124,3433,3448],"Returns the complete JSON schema and documentation for a specific operation, including all parameters, validation rules, and examples. Example Request: {\n  \"repository\": \"users\",\n  \"operation_type\": \"store\"\n} Example Response: {\n  \"success\": true,\n  \"operation\": \"store\",\n  \"type\": \"create\",\n  \"title\": \"Create User\",\n  \"description\": \"Create a new user account\",\n  \"schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"name\": {\n        \"type\": \"string\",\n        \"description\": \"The user's full name\",\n        \"required\": true\n      },\n      \"email\": {\n        \"type\": \"string\",\n        \"description\": \"The user's email address\",\n        \"required\": true\n      }\n    }\n  },\n  \"examples\": [\n    {\n      \"name\": \"John Doe\",\n      \"email\": \"john@example.com\"\n    }\n  ]\n}",{"id":3467,"title":3468,"titles":3469,"content":3470,"level":834},"/docs/mcp/mcp#_4-execute-operation","4. execute-operation",[124,3433,3448],"Executes a repository operation with the provided parameters. This is the final step after discovering the repository, listing operations, and getting operation details. Example Request: {\n  \"repository\": \"users\",\n  \"operation_type\": \"store\",\n  \"parameters\": {\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\"\n  }\n} Example Response: {\n  \"success\": true,\n  \"data\": {\n    \"id\": 123,\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\"\n  }\n}",{"id":3472,"title":3473,"titles":3474,"content":3475,"level":199},"/docs/mcp/mcp#wrapper-mode-workflow","Wrapper Mode Workflow",[124,3433],"Here's a typical workflow when an AI agent uses wrapper mode: Discover repositories: Agent calls discover-repositories to see what repositories are availableExplore operations: Agent calls get-repository-operations for the target repositoryGet schema: Agent calls get-operation-details to understand required parametersExecute: Agent calls execute-operation with the correct parameters This progressive discovery pattern reduces token usage while maintaining full functionality.",{"id":3477,"title":3478,"titles":3479,"content":3480,"level":199},"/docs/mcp/mcp#switching-between-modes","Switching Between Modes",[124,3433],"You can switch between modes at any time by updating your .env file: # Direct mode (default)\nRESTIFY_MCP_MODE=direct\n\n# Wrapper mode (token-efficient)\nRESTIFY_MCP_MODE=wrapper No code changes are required. The MCP server automatically adapts to the configured mode.",{"id":3482,"title":3483,"titles":3484,"content":3485,"level":199},"/docs/mcp/mcp#performance-considerations","Performance Considerations",[124,3433],"Direct Mode: ✅ Faster initial discovery (all tools visible immediately)❌ Higher token usage (all tools loaded into context)✅ Simpler for AI agents to understand❌ Can overwhelm context window with large applications Wrapper Mode: ✅ Dramatically lower token usage (4 tools vs 100+)✅ Scales well with large applications❌ Requires multi-step workflow✅ Better for applications with many repositories",{"id":3487,"title":1617,"titles":3488,"content":3489,"level":199},"/docs/mcp/mcp#best-practices",[124,3433],"Start with Direct Mode during development to verify all tools are working correctlySwitch to Wrapper Mode in production if you have 10+ repositories or token efficiency is importantUse wrapper mode when working with AI agents that have limited context windowsMonitor token usage to determine which mode is best for your applicationDocument your choice so team members understand which mode is active",{"id":3491,"title":3492,"titles":3493,"content":3494,"level":188},"/docs/mcp/mcp#fine-grained-tool-permissions","Fine-Grained Tool Permissions",[124],"Laravel Restify's MCP integration includes a powerful permission system that allows you to control which tools each API token can access. This is essential for multi-tenant applications or when you need to restrict AI agent capabilities.",{"id":3496,"title":3497,"titles":3498,"content":3499,"level":199},"/docs/mcp/mcp#how-permission-control-works","How Permission Control Works",[124,3492],"The RestifyServer class provides a canUseTool() method that is called whenever a tool is accessed. By default, this method returns true (all tools are accessible), but you can override it in your application server to implement custom permission logic. Key Behavior: canUseTool() is called during tool discovery (what tools the AI agent sees)canUseTool() is called during tool execution (whether the operation is allowed)In wrapper mode, permissions are checked for individual operations, not just the 4 wrapper toolsTools without permission are completely hidden from the AI agent",{"id":3501,"title":3502,"titles":3503,"content":3504,"level":199},"/docs/mcp/mcp#implementing-token-based-permissions","Implementing Token-Based Permissions",[124,3492],"Create a custom MCP server that extends RestifyServer and implements permission checks: \u003C?php\n\nnamespace App\\Mcp;\n\nuse Binaryk\\LaravelRestify\\MCP\\RestifyServer;\nuse App\\Models\\McpToken;\n\nclass ApplicationServer extends RestifyServer\n{\n    public function canUseTool(string|object $tool): bool\n    {\n        // Extract tool name from string or object\n        $toolName = is_string($tool) ? $tool : $tool->name();\n\n        // Get the API token from the request\n        $bearerToken = request()->bearerToken();\n\n        if (!$bearerToken) {\n            return false;\n        }\n\n        // Find the MCP token record\n        $mcpToken = McpToken::where('token', hash('sha256', $bearerToken))\n            ->first();\n\n        if (!$mcpToken) {\n            return false;\n        }\n\n        // Check if this tool is in the token's allowed tools list\n        // $mcpToken->allowed_tools is a JSON array like:\n        // [\"users-index\", \"posts-store\", \"posts-update-status-action\"]\n        return in_array($toolName, $mcpToken->allowed_tools ?? [], true);\n    }\n} Then register your custom server instead of the base RestifyServer: use App\\Mcp\\ApplicationServer;\nuse Laravel\\Mcp\\Facades\\Mcp;\n\nMcp::web('restify', ApplicationServer::class)->name('mcp.restify');",{"id":3506,"title":3507,"titles":3508,"content":3509,"level":199},"/docs/mcp/mcp#generating-tokens-with-tool-selection","Generating Tokens with Tool Selection",[124,3492],"To create a token creation UI, you need to show users which tools are available and let them select which ones to grant access to: use Binaryk\\LaravelRestify\\MCP\\RestifyServer;\n\n// Get all available tools\n$server = app(RestifyServer::class);\n$allTools = $server->getAllAvailableTools();\n\n// Returns a collection with all tools, regardless of mode:\n// [\n//     ['name' => 'users-index', 'title' => 'List Users', 'description' => '...', 'category' => 'CRUD Operations'],\n//     ['name' => 'users-store', 'title' => 'Create User', 'description' => '...', 'category' => 'CRUD Operations'],\n//     ['name' => 'posts-publish-action', 'title' => 'Publish Post', 'description' => '...', 'category' => 'Actions'],\n// ]\n\n// Group tools by category for better UI\n$groupedTools = $allTools->toSelectOptions();\n\n// Returns:\n// [\n//     ['category' => 'CRUD Operations', 'tools' => [...]],\n//     ['category' => 'Actions', 'tools' => [...]],\n//     ['category' => 'Getters', 'tools' => [...]],\n// ]",{"id":3511,"title":3512,"titles":3513,"content":3514,"level":199},"/docs/mcp/mcp#example-token-creation-flow","Example Token Creation Flow",[124,3492],"Here's a complete example of creating a token with specific tool permissions: use App\\Models\\McpToken;\nuse Illuminate\\Support\\Str;\n\n// 1. Show available tools to user\n$server = app(RestifyServer::class);\n$availableTools = $server->getAllAvailableTools()->toSelectOptions();\n\n// 2. User selects which tools to grant access to\n$selectedTools = [\n    'users-index',\n    'users-show',\n    'posts-index',\n    'posts-store',\n    'posts-publish-action',\n];\n\n// 3. Generate the token\n$plainTextToken = Str::random(64);\n\n// 4. Store token with permissions\n$mcpToken = McpToken::create([\n    'name' => 'AI Agent Token',\n    'token' => hash('sha256', $plainTextToken),\n    'allowed_tools' => $selectedTools, // Cast to JSON in model\n    'user_id' => auth()->id(),\n    'expires_at' => now()->addDays(30),\n]);\n\n// 5. Return plain text token to user (only shown once)\nreturn response()->json([\n    'token' => $plainTextToken,\n    'allowed_tools' => $selectedTools,\n]);",{"id":3516,"title":3517,"titles":3518,"content":3519,"level":199},"/docs/mcp/mcp#database-schema-example","Database Schema Example",[124,3492],"Here's a suggested database schema for storing MCP tokens with permissions: Schema::create('mcp_tokens', function (Blueprint $table) {\n    $table->id();\n    $table->string('name'); // Token description\n    $table->string('token')->unique(); // Hashed token\n    $table->json('allowed_tools')->nullable(); // Array of tool names\n    $table->foreignId('user_id')->constrained()->cascadeOnDelete();\n    $table->timestamp('last_used_at')->nullable();\n    $table->timestamp('expires_at')->nullable();\n    $table->timestamps();\n}); And the corresponding Eloquent model: namespace App\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass McpToken extends Model\n{\n    protected $fillable = [\n        'name',\n        'token',\n        'allowed_tools',\n        'user_id',\n        'expires_at',\n    ];\n\n    protected $casts = [\n        'allowed_tools' => 'array',\n        'expires_at' => 'datetime',\n        'last_used_at' => 'datetime',\n    ];\n\n    public function user()\n    {\n        return $this->belongsTo(User::class);\n    }\n}",{"id":3521,"title":3522,"titles":3523,"content":3524,"level":199},"/docs/mcp/mcp#permission-behavior-in-different-modes","Permission Behavior in Different Modes",[124,3492],"Direct Mode: Tools without permission are filtered out of the tools listAI agent only sees tools they have access toAttempting to use a restricted tool results in \"tool not found\" Wrapper Mode: The 4 wrapper tools are always visible (discover, get-operations, get-details, execute)When discovering repositories, only those with ≥1 accessible operation are shownWhen listing operations, only permitted operations appearAttempting to get details or execute a restricted operation throws AuthorizationException",{"id":3526,"title":3527,"titles":3528,"content":3529,"level":199},"/docs/mcp/mcp#getting-authorized-tools-at-runtime","Getting Authorized Tools at Runtime",[124,3492],"You can get only the tools the current user/token can access: $server = app(RestifyServer::class);\n$authorizedTools = $server->getAuthorizedTools();\n\n// Returns only tools that pass the canUseTool() check\n// Useful for showing \"Your API Access\" in a dashboard",{"id":3414,"title":3531,"titles":3532,"content":3533,"level":199},"Security Best Practices",[124,3492],"Always hash tokens before storing in the database (use hash('sha256', $token))Generate tokens with sufficient entropy (at least 64 random characters)Implement token expiration and enforce it in canUseTool()Log token usage by updating last_used_at on each requestRevoke tokens by deleting the database recordUse HTTPS to protect tokens in transitImplement rate limiting per token to prevent abuseAudit permission changes when updating allowed_tools html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"id":129,"title":128,"titles":3535,"content":3536,"level":182},[],"Learn how Laravel Restify repositories expose data as MCP tools using the HasMcpTools trait, enabling AI agents to query, filter, and sort records efficiently. Laravel Restify repositories provide first-class support for Model Context Protocol (MCP), enabling AI agents to efficiently interact with your APIs. This page covers MCP-specific repository features that optimize token usage and provide tailored data structures for AI consumption.",{"id":3538,"title":3539,"titles":3540,"content":3541,"level":188},"/docs/mcp/repositories#enabling-mcp-tools","Enabling MCP Tools",[128],"To provide MCP server access to your repository, you must include the HasMcpTools trait. By default, this trait provides an index tool for AI agents: use Binaryk\\LaravelRestify\\Traits\\HasMcpTools;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    use HasMcpTools;\n    \n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required()->searchable()->sortable(),\n            field('content')->string(),\n            field('status')->matchable(),\n            field('category')->matchable(),\n        ];\n    }\n    \n    public static function related(): array\n    {\n        return [\n            'author' => BelongsTo::make('user', UserRepository::class),\n            'comments' => HasMany::make('comments', CommentRepository::class), \n            'tags' => BelongsToMany::make('tags', TagRepository::class),\n        ];\n    }\n} Generated MCP Index Tool: When you include the HasMcpTools trait, Restify automatically generates an index tool. For example, with the post repository above that has matchable filters, the generated tool looks like this: {\n    \"jsonrpc\": \"2.0\",\n    \"id\": 2,\n    \"result\": {\n        \"tools\": [\n            {\n                \"name\": \"posts-index-tool\",\n                \"description\": \"Retrieve a paginated list of Post records from the posts repository with filtering, sorting, and search capabilities.\",\n                \"inputSchema\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"page\": {\n                            \"type\": \"number\",\n                            \"description\": \"Page number for pagination\"\n                        },\n                        \"perPage\": {\n                            \"type\": \"number\",\n                            \"description\": \"Number of posts per page\"\n                        },\n                        \"include\": {\n                            \"type\": \"string\",\n                            \"description\": \"Comma-separated list of relationships to include with optional field selection.\\n\\nAvailable relationships:\\n- author (fields: name)\\n- comments (fields: content)\\n- tags (fields: color, name)\\n\\nField Selection:\\nYou can specify which fields to include for each relationship using square brackets.\\nSyntax: relationship[field1|field2]\\n\\nNested Relationships:\\nYou can include deeply nested relationships using dot notation with field selection at each level.\\nSyntax: relationship[fields].nested[fields].deeper[fields] - supports unlimited nesting depth\\nNote: Field selection works at every nesting level independently.\\n\\nExamples:\\n- include=author,comments (include all fields)\\n- include=author[name] (selective fields with comma syntax)\\n- include=author[name] (selective fields with pipe syntax)\\n- include=author.posts (nested relationship - all fields)\\n- include=author[name].posts[title] (nested with field selection at each level)\\n- include=author[name|email].posts[title].tags[id] (deep nesting - 3 levels with field selection)\\n- include=author.posts[title],author.comments[body] (multiple nested from same parent)\\n- include=author[name],tags[id] (multiple relationships with field selection)\\n- include=author[email|name].posts[title],tags[id] (mixing deep nested and simple relationships)\"\n                        },\n                        \"search\": {\n                            \"type\": \"string\",\n                            \"description\": \"Search term to filter posts by title or description. Available searchable fields: title (e.g., search=term)\"\n                        },\n                        \"sort\": {\n                            \"type\": \"string\",\n                            \"description\": \"Sorting criteria for the posts. Available options: posts.id, title (e.g., sort=field or sort=-field for descending)\"\n                        },\n                        \"status\": {\n                            \"type\": \"string\",\n                            \"description\": \"Filter posts resource. Description: This is a exact match for status (e.g., status=published). It accepts negation by prefixing the column with a hyphen (e.g., -status=draft). The filter type is string.\"\n                        },\n                        \"category\": {\n                            \"type\": \"string\",\n                            \"description\": \"Filter posts resource. Description: This is a exact match for category (e.g., category=technology). It accepts negation by prefixing the column with a hyphen (e.g., -category=news). The filter type is string.\"\n                        }\n                    },\n                    \"required\": []\n                },\n                \"annotations\": {}\n            }\n        ]\n    }\n} Notice how the generated tool automatically includes: Pagination parameters (page, perPage)Relationship inclusion with field selection syntaxSearch capabilities based on searchable fields (title)Sort options from sortable fields (title)Filter parameters for each matchable field (status, category)",{"id":3543,"title":3544,"titles":3545,"content":3546,"level":188},"/docs/mcp/repositories#repository-description-for-ai-agents","Repository Description for AI Agents",[128],"Providing a clear description of your repository helps AI agents understand its purpose, domain, and capabilities. You can customize the repository description by setting the static $description property or by overriding the static description() method.",{"id":3548,"title":3549,"titles":3550,"content":3551,"level":199},"/docs/mcp/repositories#using-the-static-property","Using the Static Property",[128,3544],"The simplest way to provide a repository description is by setting the $description property: use Binaryk\\LaravelRestify\\Traits\\HasMcpTools;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    use HasMcpTools;\n\n    public static string $description = 'Manages blog posts including articles, tutorials, and news. Posts can be published, drafted, or scheduled for future publication. Each post belongs to an author and can have multiple tags and categories.';\n\n    //...\n}",{"id":3553,"title":3554,"titles":3555,"content":3556,"level":199},"/docs/mcp/repositories#using-the-description-method","Using the Description Method",[128,3544],"For dynamic descriptions or when you need more control, override the static description() method: use Binaryk\\LaravelRestify\\Traits\\HasMcpTools;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    use HasMcpTools;\n\n    public static function description(RestifyRequest $request): string\n    {\n        $userRole = $request->user()?->role;\n\n        if ($userRole === 'admin') {\n            return 'Manages all blog posts with full administrative access. Posts can be created, edited, published, or deleted. Includes moderation tools and analytics.';\n        }\n\n        return 'Manages blog posts for content creators. Authors can create and edit their own posts, submit for review, and view publication status.';\n    }\n\n    //...\n}",{"id":3558,"title":3559,"titles":3560,"content":3561,"level":199},"/docs/mcp/repositories#default-auto-generated-description","Default Auto-Generated Description",[128,3544],"If you don't provide a custom description, Restify automatically generates one based on your model structure: // For a Post model with table 'posts' and fillable fields: ['title', 'content', 'status', 'author_id']\n// Auto-generated description:\n\"This repository manages the [Post] model, which corresponds to the [posts] table in the database.\nIt provides functionalities such as listing, searching, sorting, filtering, and relationship management.\nThe model/table has the following attributes: title, content, status, author_id.\"",{"id":3563,"title":3564,"titles":3565,"content":3566,"level":199},"/docs/mcp/repositories#best-practices-for-repository-descriptions","Best Practices for Repository Descriptions",[128,3544],"Be specific about the domain: Explain what the repository manages in business termsMention key capabilities: Highlight special features like publishing workflows, approval processes, or calculationsInclude context: Describe relationships and dependencies that AI agents should know aboutKeep it concise: Aim for 2-3 sentences that provide clear context without overwhelming detail Good Example: public static string $description = 'Manages user subscriptions and billing. Handles subscription creation, upgrades, downgrades, cancellations, and payment processing through Stripe. Each subscription is linked to a user and a pricing plan.'; Poor Example: public static string $description = 'CRUD operations for subscriptions.'; // Too vague This description will be used by AI agents when deciding which tools to use and how to interact with your API, making it crucial for effective AI integration.",{"id":3568,"title":3569,"titles":3570,"content":3571,"level":188},"/docs/mcp/repositories#configuring-mcp-tools","Configuring MCP Tools",[128],"By default, the HasMcpTools trait only enables the index tool. To enable other tools like show, store, update, or delete, you need to override the corresponding methods in your repository: use Binaryk\\LaravelRestify\\Traits\\HasMcpTools;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    use HasMcpTools;\n    \n    // Index tool is enabled by default\n    public function mcpAllowsIndex(): bool\n    {\n        return true; // Default: true\n    }\n    \n    // Enable show tool for AI agents\n    public function mcpAllowsShow(): bool\n    {\n        return true; // Default: false\n    }\n    \n    // Enable store (create) tool for AI agents\n    public function mcpAllowsStore(): bool\n    {\n        return true; // Default: false\n    }\n    \n    // Enable update tool for AI agents\n    public function mcpAllowsUpdate(): bool\n    {\n        return true; // Default: false\n    }\n    \n    // Enable delete tool for AI agents\n    public function mcpAllowsDelete(): bool\n    {\n        return true; // Default: false\n    }\n    \n    // Enable action tools for AI agents\n    public function mcpAllowsActions(): bool\n    {\n        return true; // Default: false\n    }\n    \n    // Enable getter tools for AI agents\n    public function mcpAllowsGetters(): bool\n    {\n        return true; // Default: false\n    }\n}",{"id":3573,"title":3574,"titles":3575,"content":3576,"level":199},"/docs/mcp/repositories#conditional-tool-access","Conditional Tool Access",[128,3569],"You can conditionally enable tools based on user permissions or other criteria: class PostRepository extends Repository\n{\n    use HasMcpTools;\n    \n    public function mcpAllowsShow(): bool\n    {\n        return true; // Allow all authenticated users to view posts\n    }\n    \n    public function mcpAllowsStore(): bool\n    {\n        // Only allow content creators to create posts via AI\n        return request()->user()?->hasPermissionTo('create-posts') ?? false;\n    }\n    \n    public function mcpAllowsUpdate(): bool\n    {\n        // Only allow editors to update posts via AI\n        return request()->user()?->hasRole('editor') ?? false;\n    }\n    \n    public function mcpAllowsDelete(): bool\n    {\n        // Only allow admins to delete posts via AI\n        return request()->user()?->hasRole('admin') ?? false;\n    }\n    \n    public function mcpAllowsActions(): bool\n    {\n        // Enable custom actions for power users\n        return request()->user()?->hasRole(['admin', 'editor']) ?? false;\n    }\n}",{"id":3578,"title":3579,"titles":3580,"content":224,"level":188},"/docs/mcp/repositories#tool-security","Tool Security",[128],{"id":3582,"title":3583,"titles":3584,"content":3585,"level":199},"/docs/mcp/repositories#default-security-approach","Default Security Approach",[128,3579],"Restify takes a secure by default approach: Only the index tool is enabled by defaultAll other tools (show, store, update, delete) are disabled by defaultYou must explicitly enable each tool you want AI agents to accessAuthorization policies still apply to all MCP requests",{"id":3587,"title":3588,"titles":3589,"content":3590,"level":199},"/docs/mcp/repositories#best-practices-for-tool-access","Best Practices for Tool Access",[128,3579],"class PostRepository extends Repository\n{\n    use HasMcpTools;\n    \n    public function mcpAllowsShow(): bool\n    {\n        // Safe: Reading data is generally low risk\n        return true;\n    }\n    \n    public function mcpAllowsStore(): bool\n    {\n        // Moderate risk: Consider user permissions\n        return request()->user()?->can('create', Post::class) ?? false;\n    }\n    \n    public function mcpAllowsUpdate(): bool\n    {\n        // Higher risk: Require explicit permissions\n        return request()->user()?->hasPermissionTo('ai-edit-posts') ?? false;\n    }\n    \n    public function mcpAllowsDelete(): bool\n    {\n        // Highest risk: Very restrictive\n        return request()->user()?->hasRole('super-admin') ?? false;\n    }\n}",{"id":3592,"title":3593,"titles":3594,"content":3595,"level":188},"/docs/mcp/repositories#field-optimization-for-ai-agents","Field Optimization for AI Agents",[128],"For detailed information about optimizing field responses for AI agents, including MCP-specific field methods, token optimization, and conditional field visibility, see the MCP Fields documentation.",{"id":3597,"title":3598,"titles":3599,"content":3600,"level":188},"/docs/mcp/repositories#mcp-tool-examples","MCP Tool Examples",[128],"Laravel Restify automatically generates MCP tools for AI agents based on your repository configuration. Here's what the tools look like from an AI agent's perspective:",{"id":3602,"title":3603,"titles":3604,"content":3605,"level":199},"/docs/mcp/repositories#index-tool-example","Index Tool Example",[128,3598],"When you define a repository with fields and relationships, Restify generates an index tool: Repository: #[Model(Organization::class)]\nclass OrganizationRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('name')->searchable()->sortable(),\n            field('address')->matchable(),\n            field('city')->matchable(),\n            field('country')->matchable(),\n        ];\n    }\n    \n    public static function related(): array\n    {\n        return [\n            'users' => HasMany::make('users', UserRepository::class),\n            'teams' => HasMany::make('teams', TeamRepository::class), \n            'tags' => BelongsToMany::make('tags', TagRepository::class),\n        ];\n    }\n} Generated MCP Tool: {\n    \"jsonrpc\": \"2.0\",\n    \"id\": 2,\n    \"result\": {\n        \"tools\": [\n            {\n                \"name\": \"organizations-index-tool\",\n                \"description\": \"Retrieve a paginated list of Organization records from the organizations repository with filtering, sorting, and search capabilities.\",\n                \"inputSchema\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"page\": {\n                            \"type\": \"number\",\n                            \"description\": \"Page number for pagination\"\n                        },\n                        \"perPage\": {\n                            \"type\": \"number\",\n                            \"description\": \"Number of organizations per page\"\n                        },\n                        \"include\": {\n                            \"type\": \"string\",\n                            \"description\": \"Comma-separated list of relationships to include with optional field selection.\\n\\nAvailable relationships:\\n- users (fields: name)\\n- teams (fields: name)\\n- tags (fields: color, name)\\n\\nField Selection:\\nYou can specify which fields to include for each relationship using square brackets.\\nSyntax: relationship[field1|field2]\\n\\nNested Relationships:\\nYou can include deeply nested relationships using dot notation with field selection at each level.\\nSyntax: relationship[fields].nested[fields].deeper[fields] - supports unlimited nesting depth\\nNote: Field selection works at every nesting level independently.\\n\\nExamples:\\n- include=users,teams (include all fields)\\n- include=users[name] (selective fields with comma syntax)\\n- include=users[name] (selective fields with pipe syntax)\\n- include=users.posts (nested relationship - all fields)\\n- include=users[name].posts[title] (nested with field selection at each level)\\n- include=users[name|email].posts[title].tags[id] (deep nesting - 3 levels with field selection)\\n- include=users.posts[title],users.comments[body] (multiple nested from same parent)\\n- include=users[name],teams[id] (multiple relationships with field selection)\\n- include=users[email|name].posts[title],teams[id] (mixing deep nested and simple relationships)\"\n                        },\n                        \"search\": {\n                            \"type\": \"string\",\n                            \"description\": \"Search term to filter organizations by name or description. Available searchable fields: name (e.g., search=term)\"\n                        },\n                        \"sort\": {\n                            \"type\": \"string\",\n                            \"description\": \"Sorting criteria for the organizations. Available options: organizations.id, name (e.g., sort=field or sort=-field for descending)\"\n                        },\n                        \"tags\": {\n                            \"type\": \"string\",\n                            \"description\": \"Filter organizations resource. This is an exact match for tags (e.g., tags=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -tags=some_value). The filter type is string.\"\n                        },\n                        \"address\": {\n                            \"type\": \"string\",\n                            \"description\": \"Filter organizations resource. This is an exact match for address (e.g., address=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -address=some_value). The filter type is string.\"\n                        },\n                        \"city\": {\n                            \"type\": \"string\",\n                            \"description\": \"Filter organizations resource. This is an exact match for city (e.g., city=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -city=some_value). The filter type is string.\"\n                        },\n                        \"country\": {\n                            \"type\": \"string\",\n                            \"description\": \"Filter organizations resource. This is an exact match for country (e.g., country=some_value). It accepts negation by prefixing the column with a hyphen (e.g., -country=some_value). The filter type is string.\"\n                        }\n                    }\n                }\n            }\n        ]\n    }\n}",{"id":3607,"title":3608,"titles":3609,"content":3610,"level":199},"/docs/mcp/repositories#show-tool-example","Show Tool Example",[128,3598],"Generated Show Tool: {\n    \"name\": \"organizations-show-tool\",\n    \"description\": \"Retrieve a specific Organization record by ID with optional relationship loading.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"id\": {\n                \"type\": \"string\",\n                \"description\": \"The ID of the organization to retrieve\"\n            },\n            \"include\": {\n                \"type\": \"string\",\n                \"description\": \"Comma-separated list of relationships to include (e.g., include=users,teams,tags)\"\n            }\n        },\n        \"required\": [\"id\"]\n    }\n}",{"id":3612,"title":3613,"titles":3614,"content":3615,"level":199},"/docs/mcp/repositories#store-tool-example","Store Tool Example",[128,3598],"Generated Store Tool: {\n    \"name\": \"posts-store-tool\",\n    \"description\": \"Create a new Post record.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"title\": {\n                \"type\": \"string\",\n                \"description\": \"Title field for posts\"\n            },\n            \"content\": {\n                \"type\": \"string\",\n                \"description\": \"Content field for posts\"\n            },\n            \"status\": {\n                \"type\": \"string\", \n                \"description\": \"Status field for posts\"\n            },\n            \"category\": {\n                \"type\": \"string\",\n                \"description\": \"Category field for posts\"\n            }\n        },\n        \"required\": [\n            \"title\"\n        ]\n    }\n}",{"id":3617,"title":3618,"titles":3619,"content":3620,"level":199},"/docs/mcp/repositories#update-tool-example","Update Tool Example",[128,3598],"Generated Update Tool: {\n    \"name\": \"posts-update-tool\",\n    \"description\": \"Update an existing Post record by ID.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"id\": {\n                \"type\": \"string\",\n                \"description\": \"The ID of the post to update\"\n            },\n            \"title\": {\n                \"type\": \"string\",\n                \"description\": \"Title field for posts\"\n            },\n            \"content\": {\n                \"type\": \"string\",\n                \"description\": \"Content field for posts\"\n            },\n            \"status\": {\n                \"type\": \"string\",\n                \"description\": \"Status field for posts\"\n            },\n            \"category\": {\n                \"type\": \"string\", \n                \"description\": \"Category field for posts\"\n            }\n        },\n        \"required\": [\n            \"id\"\n        ]\n    }\n}",{"id":3622,"title":3623,"titles":3624,"content":3625,"level":199},"/docs/mcp/repositories#complete-tool-set","Complete Tool Set",[128,3598],"For a typical repository, Restify generates these MCP tools: {resource}-index-tool - List/search records with pagination and filtering{resource}-show-tool - Get a specific record by ID{resource}-store-tool - Create new records{resource}-update-tool - Update existing records{resource}-destroy-tool - Delete records (if authorized)",{"id":3627,"title":3628,"titles":3629,"content":3630,"level":199},"/docs/mcp/repositories#tool-features-generated-from-fields","Tool Features Generated from Fields",[128,3598],"Field Modifiers → Tool Properties: field('name')->searchable()     // → adds to \"search\" description\nfield('status')->matchable()    // → adds \"status\" filter parameter  \nfield('title')->sortable()      // → adds to \"sort\" options\nfield('email')->required()      // → adds to \"required\" array in schema Relationships → Include Options: HasMany::make('posts', PostRepository::class)     // → \"posts\" in include options\nBelongsTo::make('author', UserRepository::class)  // → \"author\" in include options Validation Rules → Schema: field('email')->rules('email')           // → type: \"string\", format: \"email\"\nfield('age')->rules('integer', 'min:18') // → type: \"integer\", minimum: 18\nfield('status')->rules('in:draft,published') // → enum: [\"draft\", \"published\"] This automatic tool generation means you define your API once in the repository, and AI agents get a complete, type-safe interface with detailed documentation and examples. This MCP repository system allows you to provide highly optimized APIs for AI agents while maintaining full functionality for human users, all from a single, unified codebase. html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .su27w, html code.shiki .su27w{--shiki-light:#916B53;--shiki-default:#916B53;--shiki-dark:#916B53}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":133,"title":132,"titles":3632,"content":3633,"level":182},[],"Learn how Laravel Restify MCP field methods like fieldsForMcpIndex and fieldsForMcpShow optimize data for AI agents, reducing token usage by up to 70%. Laravel Restify fields provide MCP-specific methods to optimize data structures for AI agent consumption, reduce token usage, and control field visibility based on user permissions.",{"id":3635,"title":3636,"titles":3637,"content":3638,"level":188},"/docs/mcp/fields#mcp-field-methods","MCP Field Methods",[132],"MCP field methods follow the same pattern as regular field methods but are prefixed with fieldsForMcp. These methods take priority over regular field methods when handling MCP requests.",{"id":3640,"title":3641,"titles":3642,"content":3643,"level":199},"/docs/mcp/fields#field-priority-for-mcp","Field Priority for MCP",[132,3636],"When an MCP request is made, Restify follows this priority order: MCP-specific methods (fieldsForMcpIndex, fieldsForMcpShow, etc.) - Highest priorityDefault fields method (fields) - Fallback This allows you to provide optimized field sets for AI agents while falling back to the standard fields method when no MCP-specific method is defined.",{"id":3645,"title":3646,"titles":3647,"content":3648,"level":199},"/docs/mcp/fields#available-mcp-field-methods","Available MCP Field Methods",[132,3636],"class PostRepository extends Repository\n{\n    // Regular fields for human consumption\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('excerpt'),\n            field('meta_description'),\n            field('tags'),\n            field('author_id'),\n            field('published_at'),\n            field('created_at'),\n            field('updated_at'),\n        ];\n    }\n    \n    // Optimized fields for AI index requests (saves 60-70% tokens)\n    public function fieldsForMcpIndex(RestifyRequest $request): array\n    {\n        return [\n            field('id'),\n            field('title'),\n            field('excerpt'),\n            field('published_at'),\n        ];\n    }\n    \n    // Focused fields for AI detail views (saves 40-50% tokens)\n    public function fieldsForMcpShow(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('author', fn() => $this->author->name),\n            field('tags'),\n            field('published_at'),\n        ];\n    }\n    \n    // Fields AI agents can use for creation\n    public function fieldsForMcpStore(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required(),\n            field('content')->required(),\n            field('excerpt'),\n            field('tags'),\n        ];\n    }\n    \n    // Fields AI agents can modify\n    public function fieldsForMcpUpdate(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('excerpt'),\n            field('tags'),\n        ];\n    }",{"id":3650,"title":3651,"titles":3652,"content":224,"level":188},"/docs/mcp/fields#token-usage-optimization","Token Usage Optimization",[132],{"id":3654,"title":3655,"titles":3656,"content":3657,"level":199},"/docs/mcp/fields#index-optimization-example","Index Optimization Example",[132,3651],"Regular fields method (for humans): public function fields(RestifyRequest $request): array\n{\n    return [\n        field('id'),\n        field('title'),\n        field('content'),\n        field('excerpt'), \n        field('meta_description'),\n        field('meta_keywords'),\n        field('author_id'),\n        field('category_id'),\n        field('status'),\n        field('featured'),\n        field('view_count'),\n        field('comment_count'),\n        field('published_at'),\n        field('created_at'),\n        field('updated_at'),\n    ]; // ~15 fields\n} MCP optimized for listing (saves ~70% tokens): public function fieldsForMcpIndex(RestifyRequest $request): array\n{\n    return [\n        field('id'),\n        field('title'),\n        field('excerpt'),\n        field('published_at'),\n        field('status'),\n    ]; // Only 5 essential fields\n}",{"id":3659,"title":3660,"titles":3661,"content":3662,"level":199},"/docs/mcp/fields#show-optimization-example","Show Optimization Example",[132,3651],"MCP optimized for detail view (saves ~50% tokens): public function fieldsForMcpShow(RestifyRequest $request): array\n{\n    return [\n        field('title'),\n        field('content'),\n        field('author_name', fn() => $this->author->name),\n        field('category', fn() => $this->category->name),\n        field('tags'),\n        field('published_at'),\n        field('status'),\n        // Removed: meta fields, timestamps, IDs, counts\n    ];\n}",{"id":3664,"title":3665,"titles":3666,"content":3667,"level":188},"/docs/mcp/fields#mcp-aware-relationships","MCP-Aware Relationships",[132],"Handle relationships efficiently for AI agents: class PostRepository extends Repository\n{\n    public static function related(): array\n    {\n        return [\n            'author' => BelongsTo::make('user', UserRepository::class),\n            'comments' => HasMany::make('comments', CommentRepository::class),\n            'tags' => BelongsToMany::make('tags', TagRepository::class),\n        ];\n    }\n    \n    // AI agents get optimized relationship data\n    public function fieldsForMcpShow(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            // Inline relationship data to reduce API calls\n            field('author_name', fn() => $this->user->name),\n            field('author_email', fn() => $this->user->email),\n            field('comment_count', fn() => $this->comments()->count()),\n            field('tag_names', fn() => $this->tags->pluck('name')->toArray()),\n        ];\n    }\n}",{"id":3669,"title":3670,"titles":3671,"content":3672,"level":188},"/docs/mcp/fields#conditional-mcp-fields","Conditional MCP Fields",[132],"Provide different fields based on AI agent context or user permissions using field-level visibility controls:",{"id":3674,"title":3675,"titles":3676,"content":3677,"level":199},"/docs/mcp/fields#using-cansee-method","Using canSee() Method",[132,3670],"class PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required()->searchable()->sortable(),\n            field('content')->string(),\n            field('status')->matchable(),\n            field('category')->matchable(),\n            \n            // Admin-only fields\n            field('internal_notes')\n                ->canSee(fn($request) => $request->user()->hasRole('admin')),\n                \n            field('performance_metrics', fn() => [\n                'views' => $this->view_count,\n                'engagement' => $this->calculateEngagement(),\n                'conversion_rate' => $this->calculateConversion(),\n            ])->canSee(fn($request) => $request->user()->hasRole('admin')),\n            \n            // Content manager fields\n            field('author_info', fn() => [\n                'name' => $this->author->name,\n                'email' => $this->author->email,\n                'posts_count' => $this->author->posts_count,\n            ])->canSee(fn($request) => $request->user()->hasPermissionTo('manage-content')),\n        ];\n    }\n}",{"id":3679,"title":3680,"titles":3681,"content":3682,"level":199},"/docs/mcp/fields#using-hidefrommcp-method","Using hideFromMcp() Method",[132,3670],"When you want to hide certain fields specifically from MCP requests (while keeping them for REST endpoints), use the hideFromMcp() method: class PostRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('title')->required()->searchable()->sortable(),\n            field('content')->string(),\n            field('status')->matchable(),\n            field('category')->matchable(),\n            \n            // Hide sensitive data from AI agents\n            field('internal_notes')\n                ->hideFromMcp(fn($request) => !$request->user()->hasRole('admin')),\n                \n            // Always hide from MCP\n            field('secret_api_key')->hideFromMcp(),\n            \n            // Conditionally hide from MCP based on user role\n            field('draft_content')\n                ->hideFromMcp(fn($request) => !$request->user()->hasPermissionTo('view-drafts')),\n                \n            // Hide heavy computational fields from MCP to save tokens\n            field('full_statistics', fn() => $this->generateDetailedStats())\n                ->hideFromMcp(), // Use dedicated MCP fields instead\n        ];\n    }\n    \n    // Use dedicated MCP field method for optimized AI responses\n    public function fieldsForMcpShow(RestifyRequest $request): array\n    {\n        return [\n            field('title'),\n            field('content'),\n            field('status'),\n            field('category'),\n            // Light-weight stats for AI agents\n            field('basic_stats', fn() => [\n                'views' => $this->view_count,\n                'comments' => $this->comments_count,\n            ]),\n        ];\n    }\n}",{"id":3684,"title":3685,"titles":3686,"content":3687,"level":199},"/docs/mcp/fields#benefits-of-field-level-control","Benefits of Field-Level Control",[132,3670],"No duplicate logic: Define visibility rules once in the main fields() methodAutomatic application: Rules apply to all MCP operations (index, show, store, update)Flexible conditions: Use any logic in the callback to determine visibilityPerformance optimization: Hide expensive computed fields from AI agentsSecurity: Keep sensitive data away from AI agents while maintaining REST functionality",{"id":3689,"title":3690,"titles":3691,"content":3692,"level":188},"/docs/mcp/fields#testing-mcp-field-methods","Testing MCP Field Methods",[132],"Test your MCP-specific field methods to ensure they work correctly: class McpRepositoryTest extends TestCase\n{\n    public function test_mcp_index_fields_are_optimized()\n    {\n        $repository = new PostRepository();\n        $mcpRequest = new McpRequest();\n        $mcpRequest->setIsIndexRequest(true);\n        \n        $fields = $repository->collectFields($mcpRequest);\n        \n        // Assert only essential fields are returned\n        $this->assertCount(4, $fields);\n        $this->assertTrue($fields->contains('attribute', 'title'));\n        $this->assertTrue($fields->contains('attribute', 'excerpt'));\n        $this->assertFalse($fields->contains('attribute', 'meta_description'));\n    }\n    \n    public function test_mcp_fields_fall_back_to_regular_fields()\n    {\n        $repository = new PostRepository();\n        $mcpRequest = new McpRequest();\n        \n        // Remove MCP method to test fallback\n        $fields = $repository->collectFields($mcpRequest);\n        \n        // Should fall back to regular fields method\n        $this->assertGreaterThan(4, $fields->count());\n    }\n}",{"id":3694,"title":3695,"titles":3696,"content":3697,"level":188},"/docs/mcp/fields#mcp-performance-monitoring","MCP Performance Monitoring",[132],"Monitor token usage and performance of your MCP endpoints: class PostRepository extends Repository\n{\n    public function fieldsForMcpIndex(RestifyRequest $request): array\n    {\n        $startTime = microtime(true);\n        \n        $fields = [\n            field('id'),\n            field('title'),\n            field('excerpt'),\n            field('published_at'),\n        ];\n        \n        // Log performance metrics for AI optimization\n        Log::info('MCP Index Fields', [\n            'repository' => static::class,\n            'field_count' => count($fields),\n            'execution_time' => microtime(true) - $startTime,\n            'user_agent' => $request->userAgent(),\n        ]);\n        \n        return $fields;\n    }\n}",{"id":3699,"title":3700,"titles":3701,"content":3702,"level":188},"/docs/mcp/fields#file-field-with-custom-filenames","File Field with Custom Filenames",[132],"The File field supports custom filenames from request data, perfect for automation workflows like n8n where you want to control the filename during upload.",{"id":3704,"title":3705,"titles":3706,"content":3707,"level":199},"/docs/mcp/fields#basic-file-upload","Basic File Upload",[132,3700],"class ExpenseRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('receipt_path')->file()\n                ->path('expense_receipts/'.Auth::id())\n                ->storeOriginalName('receipt_filename')\n                ->storeSize('receipt_size')\n                ->deletable()\n                ->disk('s3'),\n\n            field('receipt_filename')\n                ->description('Original filename of the uploaded receipt.'),\n\n            field('receipt_size')\n                ->description('Size of the uploaded receipt in bytes.'),\n        ];\n    }\n}",{"id":3709,"title":3710,"titles":3711,"content":3712,"level":199},"/docs/mcp/fields#custom-filename-from-request","Custom Filename from Request",[132,3700],"Use storeAs() with a callback to read the custom filename from the request: field('receipt_path')->file()\n    ->path('expense_receipts/'.Auth::id())\n    ->storeAs(fn($request) => $request->input('receipt_filename'))\n    ->storeOriginalName('receipt_filename')\n    ->storeSize('receipt_size')\n    ->deletable()\n    ->disk('s3'),\n\nfield('receipt_filename')\n    ->description('Custom filename for the receipt. Provide a meaningful name.'),",{"id":3714,"title":3715,"titles":3716,"content":3717,"level":199},"/docs/mcp/fields#smart-extension-handling","Smart Extension Handling",[132,3700],"The File field automatically handles file extensions: // Request: receipt_filename = \"Invoice_Jan_2024\"\n// Uploaded file: expense.pdf\n// Result: Invoice_Jan_2024.pdf (extension auto-appended)\n\n// Request: receipt_filename = \"Invoice_Jan_2024.pdf\"\n// Uploaded file: expense.pdf\n// Result: Invoice_Jan_2024.pdf (used as-is)\n\n// Request: receipt_filename = \"\" or null\n// Uploaded file: expense.pdf\n// Result: a1b2c3d4e5f6.pdf (fallback to auto-generated hash)",{"id":3719,"title":3720,"titles":3721,"content":3722,"level":199},"/docs/mcp/fields#file-field-behaviors","File Field Behaviors",[132,3700],"With Callable storeAs(): ->storeAs(fn($request) => $request->input('custom_name')) Uses the returned filename for storageUses the same filename for storeOriginalName() columnAuto-appends extension if missingFalls back to auto-generated name if returns empty/null With Static storeAs(): ->storeAs('avatar.jpg') Uses the static filename for storageUses uploaded file's original name for storeOriginalName() columnExtension must be included in the static string Without storeAs(): field('receipt')->file() Auto-generates hash-based filenameUses uploaded file's original name for storeOriginalName() column",{"id":3724,"title":3725,"titles":3726,"content":3727,"level":199},"/docs/mcp/fields#mcp-optimized-file-fields","MCP-Optimized File Fields",[132,3700],"Hide file fields from MCP responses to reduce token usage: class ExpenseRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            field('receipt_path')->file()\n                ->path('expense_receipts/'.Auth::id())\n                ->storeAs(fn($request) => $request->input('receipt_filename'))\n                ->storeOriginalName('receipt_filename')\n                ->resolveUsingTemporaryUrl($request->boolean('temporary_urls'))\n                ->hideFromMcp()\n                ->description('Only send the absolute URL if you have it, otherwise do not send this field.')\n                ->disk('s3'),\n\n            field('receipt_filename')\n                ->description('Filename of the receipt. Keep it descriptive.'),\n        ];\n    }\n}",{"id":3729,"title":3730,"titles":3731,"content":3732,"level":199},"/docs/mcp/fields#file-upload-automation-example","File Upload Automation Example",[132,3700],"Perfect for n8n workflows where files are extracted from emails: {\n  \"receipt_path\": \"\u003Cuploaded_file>\",\n  \"receipt_filename\": \"Invoice_ABC_Company_Jan_2024\",\n  \"amount\": 1500.00,\n  \"date\": \"2024-01-15\",\n  \"vendor\": \"ABC Company\"\n} The file will be stored as expense_receipts/123/Invoice_ABC_Company_Jan_2024.pdf and the receipt_filename column will contain Invoice_ABC_Company_Jan_2024.pdf.",{"id":3734,"title":3735,"titles":3736,"content":3737,"level":188},"/docs/mcp/fields#base64-file-field","Base64 File Field",[132],"The Base64File field handles base64-encoded file data, perfect for modern frontend applications that send images as data URIs (signature pads, image editors, webcam captures, canvas-based drawing).",{"id":3739,"title":1529,"titles":3740,"content":3741,"level":199},"/docs/mcp/fields#basic-usage",[132,3735],"use Binaryk\\LaravelRestify\\Fields\\Base64File;\n\nclass SignatureRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            Base64File::make('signature')\n                ->path('signatures/'.$request->user()->id)\n                ->disk('s3'),\n        ];\n    }\n}",{"id":3743,"title":3744,"titles":3745,"content":3746,"level":199},"/docs/mcp/fields#request-payload","Request Payload",[132,3735],"Send base64 data URI strings directly: {\n  \"signature\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n}",{"id":3748,"title":3749,"titles":3750,"content":3751,"level":199},"/docs/mcp/fields#features","Features",[132,3735],"The Base64File field inherits all capabilities from the File field: Base64File::make('signature')\n    ->path('signatures')                           // Storage subdirectory\n    ->disk('s3')                                   // Storage disk\n    ->storeAs(fn($request) => 'custom-name')       // Custom filename\n    ->storeOriginalName('signature_filename')      // Store filename in column\n    ->storeSize('signature_size')                  // Store file size in column\n    ->resolveUsingTemporaryUrl(true, now()->addMinutes(30))  // S3 signed URLs\n    ->resolveUsingFullUrl()                        // Full storage URL\n    ->prunable()                                   // Delete file when model deleted\n    ->deletable()                                  // Allow manual deletion",{"id":3753,"title":3754,"titles":3755,"content":3756,"level":199},"/docs/mcp/fields#mime-type-detection","MIME Type Detection",[132,3735],"The field automatically detects the file extension from the data URI header: Data URI ContainsExtensionimage/png.pngimage/jpeg or image/jpg.jpgimage/gif.gifimage/webp.webpimage/svg+xml.svgapplication/pdf.pdftext/plain.txtapplication/json.json(default).bin",{"id":3758,"title":3759,"titles":3760,"content":3761,"level":199},"/docs/mcp/fields#fallback-behavior","Fallback Behavior",[132,3735],"The Base64File field gracefully handles different input types: Base64 data URI: Decodes and stores as fileURL: Delegates to parent File field behavior (stores URL directly)File upload: Delegates to parent File field behavior (normal upload)Empty/null/invalid: Ignores and preserves existing value",{"id":3763,"title":3764,"titles":3765,"content":3766,"level":199},"/docs/mcp/fields#custom-filename","Custom Filename",[132,3735],"Control the stored filename: // Static filename (extension auto-appended)\nBase64File::make('signature')\n    ->storeAs('user-signature')  // Stored as: user-signature.png\n\n// From callback (extension auto-appended)\nBase64File::make('signature')\n    ->storeAs(fn($request) => \"sig-{$request->user()->id}\")\n\n// With extension included\nBase64File::make('signature')\n    ->storeAs('signature.png')  // Used as-is",{"id":3768,"title":3769,"titles":3770,"content":3771,"level":199},"/docs/mcp/fields#original-name-and-size","Original Name and Size",[132,3735],"Track metadata in separate columns: Base64File::make('signature')\n    ->storeOriginalName('signature_filename')  // Stores: \"base64-upload.png\" or custom name\n    ->storeSize('signature_size')              // Stores: decoded file size in bytes When using storeAs() with a callback, the custom filename is used for storeOriginalName().",{"id":3773,"title":3774,"titles":3775,"content":3776,"level":199},"/docs/mcp/fields#prunable-files","Prunable Files",[132,3735],"Automatically delete the file when the model is deleted: Base64File::make('signature')\n    ->prunable() When updating a model with a new base64 file and prunable() is enabled, the old file is automatically deleted.",{"id":3778,"title":3207,"titles":3779,"content":3780,"level":199},"/docs/mcp/fields#use-cases",[132,3735],"Signature pads: Users draw signatures on canvasImage editors: Cropped/edited images from canvasWebcam captures: Photos taken in-browserScreenshot tools: Clipboard image pasteAvatar generators: Generated avatars from canvasDrawing applications: User-created artwork",{"id":3782,"title":679,"titles":3783,"content":3784,"level":199},"/docs/mcp/fields#mcp-integration",[132,3735],"Hide base64 fields from MCP responses or use optimized fields: class SignatureRepository extends Repository\n{\n    public function fields(RestifyRequest $request): array\n    {\n        return [\n            Base64File::make('signature')\n                ->path('signatures')\n                ->resolveUsingTemporaryUrl(true, now()->addMinutes(30))\n                ->hideFromMcp()  // Hide from AI agents\n                ->disk('s3'),\n\n            field('signature_filename')\n                ->description('Filename of the stored signature'),\n        ];\n    }\n\n    // Optimized fields for MCP\n    public function fieldsForMcpShow(RestifyRequest $request): array\n    {\n        return [\n            field('signature_url', fn() => Storage::disk('s3')->temporaryUrl(\n                $this->signature,\n                now()->addMinutes(30)\n            )),\n        ];\n    }\n}",{"id":3786,"title":3787,"titles":3788,"content":3789,"level":199},"/docs/mcp/fields#raw-base64-support","Raw Base64 Support",[132,3735],"The field also supports raw base64 strings without the data URI prefix: {\n  \"signature\": \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"\n} Note: Without the data URI prefix, the field cannot detect the MIME type and will use .bin as the extension. Always prefer the full data URI format.",{"id":3791,"title":1617,"titles":3792,"content":224,"level":188},"/docs/mcp/fields#best-practices",[132],{"id":3794,"title":3795,"titles":3796,"content":3797,"level":199},"/docs/mcp/fields#_1-field-selection-strategy","1. Field Selection Strategy",[132,1617],"Index: Include only essential fields for listing (id, title, status, dates)Show: Focus on content fields, inline simple relationshipsStore/Update: Include only fields AI agents should modify",{"id":3799,"title":3800,"titles":3801,"content":3802,"level":199},"/docs/mcp/fields#_2-token-optimization","2. Token Optimization",[132,1617],"Remove unnecessary metadata fieldsInline simple relationship data instead of separate API callsUse computed fields to provide aggregated informationAvoid deeply nested relationship structuresHide file fields from MCP using hideFromMcp() to save tokens",{"id":3804,"title":3805,"titles":3806,"content":3807,"level":199},"/docs/mcp/fields#_3-security-considerations","3. Security Considerations",[132,1617],"Same authorization rules apply to MCP requestsUse conditional fields based on AI agent permissionsLog AI agent activities for audit purposesValidate AI agent inputs thoroughly",{"id":3809,"title":3810,"titles":3811,"content":3812,"level":199},"/docs/mcp/fields#_4-development-workflow","4. Development Workflow",[132,1617],"Start with regular field methodsIdentify token-heavy operations through monitoringCreate MCP-specific methods for optimizationTest both human and AI agent access patternsMonitor and iterate based on usage patterns This MCP field system allows you to provide highly optimized data structures for AI agents while maintaining full functionality for human users, all from a single, unified codebase. html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":137,"title":136,"titles":3814,"content":3815,"level":182},[],"Learn how to expose Laravel Restify Getters to AI agents via MCP, with validation rules, JSON Schema generation, hideFromMcp(), and security best practices. Laravel Restify Getters can be exposed to AI agents through the Model Context Protocol (MCP), enabling intelligent data retrieval and analytics. This page covers how to configure getters for MCP integration and optimize them for AI consumption.",{"id":3817,"title":3818,"titles":3819,"content":3820,"level":188},"/docs/mcp/getters#enabling-mcp-getters","Enabling MCP Getters",[136],"To expose getters to the MCP server, you must enable them in your repository by overriding the mcpAllowsGetters() method: use Binaryk\\LaravelRestify\\Traits\\HasMcpTools;\n\n#[Model(User::class)]\nclass UserRepository extends Repository\n{\n    use HasMcpTools;\n\n    // Enable getter tools for AI agents\n    public function mcpAllowsGetters(): bool\n    {\n        return true; // Default: false\n    }\n\n    public function getters(RestifyRequest $request): array\n    {\n        return [\n            StripeInformationGetter::new()->onlyOnShow(),\n            UserAnalyticsGetter::new()->onlyOnShow(),\n            EngagementMetricsGetter::new()->onlyOnShow(),\n        ];\n    }\n} Once enabled, all getters defined in the getters() method will be automatically exposed as MCP tools to AI agents.",{"id":3822,"title":3823,"titles":3824,"content":3825,"level":188},"/docs/mcp/getters#getter-descriptions-for-ai-agents","Getter Descriptions for AI Agents",[136],"Providing clear descriptions helps AI agents understand when and how to retrieve data using your getters. You can customize getter descriptions using the $description property or by overriding the description() method.",{"id":3827,"title":3828,"titles":3829,"content":3830,"level":199},"/docs/mcp/getters#using-the-description-property","Using the Description Property",[136,3823],"use Binaryk\\LaravelRestify\\Getters\\Getter;\nuse Illuminate\\Http\\Request;\n\nclass StripeInformationGetter extends Getter\n{\n    public string $description = 'Retrieve Stripe customer information including subscription status, payment methods, and billing history for a specific user.';\n\n    public function handle(Request $request, User $user)\n    {\n        return response()->json([\n            'data' => $user->asStripeCustomer(),\n        ]);\n    }\n}",{"id":3832,"title":3554,"titles":3833,"content":3834,"level":199},"/docs/mcp/getters#using-the-description-method",[136,3823],"For dynamic descriptions based on context: class UserAnalyticsGetter extends Getter\n{\n    public function description(RestifyRequest $request): string\n    {\n        $userRole = $request->user()?->role;\n\n        if ($userRole === 'admin') {\n            return 'Retrieve comprehensive user analytics including activity metrics, engagement scores, and revenue attribution.';\n        }\n\n        return 'Retrieve basic user analytics including login history and profile completion status.';\n    }\n\n    //...\n}",{"id":3836,"title":857,"titles":3837,"content":3838,"level":188},"/docs/mcp/getters#validation-rules-for-ai-schema",[136],"The rules() method is crucial for MCP integration. Restify automatically converts your Laravel validation rules into JSON Schema that AI agents understand and use for parameter validation. class UserAnalyticsGetter extends Getter\n{\n    public string $description = 'Get user analytics and activity metrics for a specific date range';\n\n    public function rules(): array\n    {\n        return [\n            'start_date' => ['required', 'date', 'before:end_date'],\n            'end_date' => ['required', 'date', 'after:start_date'],\n            'metrics' => ['array'],\n            'metrics.*' => ['string', 'in:views,clicks,conversions,revenue'],\n            'granularity' => ['string', 'in:daily,weekly,monthly'],\n            'include_comparison' => ['boolean'],\n        ];\n    }\n\n    public function handle(Request $request, User $user)\n    {\n        $validated = $request->validate($this->rules());\n\n        return response()->json([\n            'data' => $user->analytics($validated),\n        ]);\n    }\n} The AI agent will automatically receive a JSON Schema with: start_date: date string (required, must be before end_date)end_date: date string (required, must be after start_date)metrics: array (optional)metrics.*: string items (must be one of: views, clicks, conversions, revenue)granularity: string (must be one of: daily, weekly, monthly)include_comparison: boolean (optional)",{"id":3840,"title":3841,"titles":3842,"content":3843,"level":188},"/docs/mcp/getters#getter-scope-and-mcp-enforcement","Getter Scope and MCP Enforcement",[136],"Unlike Actions, Getters in Laravel Restify are designed specifically for single resource data retrieval. Getters always operate on a single model instance and require AI agents to provide an id parameter. Getters do not support batch operations (resources parameter) or standalone mode like Actions do.",{"id":3845,"title":3846,"titles":3847,"content":3848,"level":199},"/docs/mcp/getters#important-differences-from-actions","Important Differences from Actions",[136,3841],"No standalone() method: Getters always operate on a single resourceNo onlyOnIndex() method: Getters don't support batch operationsNo resources parameter: AI agents never provide a list of IDsAlways uses onlyOnShow(): This is the only scope modifier for getters",{"id":3850,"title":3851,"titles":3852,"content":3853,"level":199},"/docs/mcp/getters#show-getters-single-resource-only","Show Getters (Single Resource Only)",[136,3841],"Getters retrieve data for a single resource. When using onlyOnShow(), AI agents are required to provide an id parameter: class StripeInformationGetter extends Getter\n{\n    public string $description = 'Retrieve detailed Stripe customer information and subscription data for a specific user';\n\n    public function rules(): array\n    {\n        return [\n            'include_invoices' => ['boolean'],\n            'include_payment_methods' => ['boolean'],\n        ];\n    }\n\n    public function handle(Request $request, User $user)\n    {\n        $validated = $request->validate($this->rules());\n\n        return response()->json([\n            'data' => [\n                'customer' => $user->asStripeCustomer(),\n                'invoices' => $validated['include_invoices'] ?? false\n                    ? $user->invoices()\n                    : null,\n                'payment_methods' => $validated['include_payment_methods'] ?? false\n                    ? $user->paymentMethods()\n                    : null,\n            ],\n        ]);\n    }\n}\n\n// In UserRepository\npublic function getters(RestifyRequest $request): array\n{\n    return [\n        StripeInformationGetter::new()->onlyOnShow(),\n    ];\n} Generated MCP Tool Schema: {\n    \"name\": \"users-stripe-information-getter-tool\",\n    \"description\": \"Execute Stripe Information getter to retrieve data for a specific User record in the users repository.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"id\": {\n                \"type\": \"string\",\n                \"title\": \"id\",\n                \"description\": \"The ID of the resource (User) to perform the getter on.\",\n                \"required\": true\n            },\n            \"include_invoices\": {\n                \"type\": \"boolean\"\n            },\n            \"include_payment_methods\": {\n                \"type\": \"boolean\"\n            }\n        },\n        \"required\": [\"id\"]\n    }\n} AI Agent Usage: // Must provide the id\n{\n    \"id\": \"123\",\n    \"include_invoices\": true,\n    \"include_payment_methods\": true\n}",{"id":3855,"title":3856,"titles":3857,"content":3858,"level":188},"/docs/mcp/getters#complete-getter-example","Complete Getter Example",[136],"Here's a comprehensive example showing all aspects of MCP-optimized getters: use Binaryk\\LaravelRestify\\Getters\\Getter;\nuse Illuminate\\Http\\Request;\n\nclass UserEngagementMetricsGetter extends Getter\n{\n    // Clear description for AI agents\n    public string $description = 'Retrieve detailed engagement metrics for a user including login frequency, feature usage, and interaction patterns over a specified time period.';\n\n    // Comprehensive validation rules converted to JSON Schema\n    public function rules(): array\n    {\n        return [\n            'start_date' => ['required', 'date', 'before:end_date'],\n            'end_date' => ['required', 'date', 'after:start_date', 'before_or_equal:today'],\n            'metrics' => ['array'],\n            'metrics.*' => ['string', 'in:logins,features_used,time_spent,interactions'],\n            'granularity' => ['string', 'in:hourly,daily,weekly'],\n            'include_breakdown' => ['boolean'],\n            'compare_to_average' => ['boolean'],\n        ];\n    }\n\n    public function handle(Request $request, User $user)\n    {\n        $validated = $request->validate($this->rules());\n\n        $metrics = $user->engagementMetrics(\n            $validated['start_date'],\n            $validated['end_date'],\n            $validated['metrics'] ?? ['logins', 'features_used'],\n            $validated['granularity'] ?? 'daily'\n        );\n\n        $response = [\n            'user_id' => $user->id,\n            'period' => [\n                'start' => $validated['start_date'],\n                'end' => $validated['end_date'],\n            ],\n            'metrics' => $metrics,\n        ];\n\n        if ($validated['include_breakdown'] ?? false) {\n            $response['breakdown'] = $user->engagementBreakdown($validated);\n        }\n\n        if ($validated['compare_to_average'] ?? false) {\n            $response['platform_average'] = User::averageEngagement($validated);\n            $response['percentile'] = $user->engagementPercentile($validated);\n        }\n\n        return response()->json(['data' => $response]);\n    }\n}\n\n// In UserRepository\npublic function getters(RestifyRequest $request): array\n{\n    return [\n        UserEngagementMetricsGetter::new()->onlyOnShow(),\n    ];\n}",{"id":3860,"title":3861,"titles":3862,"content":3863,"level":188},"/docs/mcp/getters#hiding-getters-from-mcp","Hiding Getters from MCP",[136],"You can selectively hide getters from MCP while keeping them available for regular API requests. This is useful when you want certain data to be accessible only through manual user requests but not to AI agents.",{"id":3865,"title":3866,"titles":3867,"content":3868,"level":199},"/docs/mcp/getters#using-hidefrommcp","Using hideFromMcp()",[136,3861],"public function getters(RestifyRequest $request): array\n{\n    return [\n        // Available to both API and MCP\n        UserAnalyticsGetter::new()->onlyOnShow(),\n\n        // Available only to API requests, hidden from AI agents\n        StripeInformationGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n\n        // Conditionally hide based on environment\n        DebugInformationGetter::new()\n            ->hideFromMcp(app()->environment('production'))\n            ->onlyOnShow(),\n\n        // Available only in development for AI testing\n        TestDataGetter::new()\n            ->hideFromMcp(! app()->environment('local'))\n            ->onlyOnShow(),\n    ];\n}",{"id":3870,"title":3871,"titles":3872,"content":3873,"level":199},"/docs/mcp/getters#use-cases-for-hiding-getters","Use Cases for Hiding Getters",[136,3861],"1. Sensitive Financial Data public function getters(RestifyRequest $request): array\n{\n    return [\n        // Never expose payment details to AI\n        PaymentMethodsGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n\n        // Never expose financial reports to AI\n        RevenueBreakdownGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n    ];\n} 2. Personal Identifiable Information (PII) public function getters(RestifyRequest $request): array\n{\n    return [\n        // Require human oversight for PII access\n        FullUserDetailsGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n\n        // Hide social security numbers from AI\n        ComplianceDataGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n    ];\n} 3. Debug and Internal Data public function getters(RestifyRequest $request): array\n{\n    return [\n        // Hide debug info in production\n        SystemDiagnosticsGetter::new()\n            ->hideFromMcp(app()->environment('production'))\n            ->onlyOnShow(),\n\n        // Internal metrics not for AI consumption\n        InternalMetricsGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n    ];\n} 4. Rate-Limited or Expensive Operations public function getters(RestifyRequest $request): array\n{\n    return [\n        // Prevent AI from triggering expensive external API calls\n        ExternalDataSyncGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n\n        // Rate-limited third-party service\n        ThirdPartyIntegrationGetter::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n    ];\n}",{"id":3875,"title":3876,"titles":3877,"content":3878,"level":199},"/docs/mcp/getters#combining-with-other-modifiers","Combining with Other Modifiers",[136,3861],"The hideFromMcp() method works seamlessly with other getter modifiers: public function getters(RestifyRequest $request): array\n{\n    return [\n        StripeInformationGetter::new()\n            ->hideFromMcp()                    // Hide from AI agents\n            ->onlyOnShow()                     // Only for single users\n            ->canSee(fn() => $request->user()->can('view-billing')),  // Authorization\n\n        SensitiveAnalyticsGetter::new()\n            ->hideFromMcp(app()->environment('production'))  // Conditional hiding\n            ->onlyOnShow()\n            ->canSee(fn() => $request->user()->isAdmin()),\n    ];\n} Hidden getters are completely excluded from MCP tool discovery. AI agents will not see them in the available tools list and cannot execute them, even if they somehow know the getter URI key. Use `hideFromMcp()` liberally for sensitive data retrieval. It's better to be conservative and explicitly expose getters to AI rather than accidentally exposing sensitive information.",{"id":3880,"title":3881,"titles":3882,"content":3883,"level":188},"/docs/mcp/getters#conditional-getter-visibility","Conditional Getter Visibility",[136],"You can control which getters are visible to AI agents based on permissions: class UserRepository extends Repository\n{\n    use HasMcpTools;\n\n    public function mcpAllowsGetters(): bool\n    {\n        // Only enable getters for specific roles\n        return request()->user()?->hasRole(['admin', 'analyst']) ?? false;\n    }\n\n    public function getters(RestifyRequest $request): array\n    {\n        return [\n            // Only admins can see financial data\n            StripeInformationGetter::new()\n                ->canSee(fn() => $request->user()?->hasRole('admin')),\n\n            // Analysts and admins can see analytics\n            UserAnalyticsGetter::new()\n                ->canSee(fn() => $request->user()?->hasRole(['admin', 'analyst'])),\n\n            // Everyone with getter access can see basic stats\n            ActiveStatusGetter::new(),\n\n            // Hidden from AI but available to API\n            SensitiveDataGetter::new()\n                ->hideFromMcp(),\n        ];\n    }\n}",{"id":3885,"title":3886,"titles":3887,"content":224,"level":188},"/docs/mcp/getters#best-practices-for-mcp-getters","Best Practices for MCP Getters",[136],{"id":3889,"title":3890,"titles":3891,"content":3892,"level":199},"/docs/mcp/getters#_1-write-clear-descriptions","1. Write Clear Descriptions",[136,3886],"// Good: Specific, explains what data is retrieved\npublic string $description = 'Retrieve user subscription details including plan type, billing cycle, next payment date, and usage limits. Includes historical subscription changes if requested.';\n\n// Poor: Too vague\npublic string $description = 'Get user subscription';",{"id":3894,"title":3895,"titles":3896,"content":3897,"level":199},"/docs/mcp/getters#_2-use-comprehensive-validation-rules","2. Use Comprehensive Validation Rules",[136,3886],"// Good: Detailed validation that generates rich JSON Schema\npublic function rules(): array\n{\n    return [\n        'date_range' => ['required', 'string', 'in:7d,30d,90d,1y,custom'],\n        'start_date' => ['required_if:date_range,custom', 'date'],\n        'end_date' => ['required_if:date_range,custom', 'date', 'after:start_date'],\n        'metrics' => ['array', 'min:1'],\n        'metrics.*' => ['string', 'in:revenue,users,churn,mrr'],\n        'group_by' => ['nullable', 'string', 'in:day,week,month'],\n    ];\n}\n\n// Poor: Minimal validation\npublic function rules(): array\n{\n    return [\n        'metrics' => ['array'],\n    ];\n}",{"id":3899,"title":3900,"titles":3901,"content":3902,"level":199},"/docs/mcp/getters#_3-always-use-onlyonshow","3. Always Use onlyOnShow()",[136,3886],"// Getters always operate on a single resource and use onlyOnShow()\nUserAnalyticsGetter::new()->onlyOnShow()\n\n// Even for aggregate data, the getter retrieves it for a specific resource\nStripeInformationGetter::new()->onlyOnShow()\n\n// All getters require the AI to provide an id parameter\nEngagementMetricsGetter::new()->onlyOnShow()",{"id":3904,"title":3905,"titles":3906,"content":3907,"level":199},"/docs/mcp/getters#_4-optimize-response-size-for-ai-agents","4. Optimize Response Size for AI Agents",[136,3886],"public function handle(Request $request, User $user)\n{\n    $validated = $request->validate($this->rules());\n\n    // Only include requested data to minimize token usage\n    $response = ['user_id' => $user->id];\n\n    if ($validated['include_profile'] ?? false) {\n        $response['profile'] = $user->profile;\n    }\n\n    if ($validated['include_subscriptions'] ?? false) {\n        $response['subscriptions'] = $user->subscriptions;\n    }\n\n    if ($validated['include_activity'] ?? false) {\n        $response['activity'] = $user->recentActivity(\n            $validated['activity_limit'] ?? 10\n        );\n    }\n\n    return response()->json(['data' => $response]);\n}",{"id":3909,"title":3910,"titles":3911,"content":3912,"level":199},"/docs/mcp/getters#_5-cache-expensive-operations","5. Cache Expensive Operations",[136,3886],"public function handle(Request $request)\n{\n    $validated = $request->validate($this->rules());\n\n    $cacheKey = \"platform_stats_{$validated['period']}\";\n\n    return response()->json([\n        'data' => Cache::remember($cacheKey, 300, function () use ($validated) {\n            return [\n                'total_users' => User::count(),\n                'active_users' => User::active()->count(),\n                'revenue' => $this->calculateRevenue($validated['period']),\n                'growth' => $this->calculateGrowth($validated['period']),\n            ];\n        }),\n    ]);\n}",{"id":3914,"title":3410,"titles":3915,"content":3916,"level":188},"/docs/mcp/getters#security-considerations",[136],"Validate all input: Always validate using $request->validate($this->rules())Check permissions: Ensure users can only access data they're authorized to seeSanitize output: Don't expose sensitive data unnecessarilyRate limit: Consider rate limiting for expensive getters public function handle(Request $request, User $user)\n{\n    // Validate input\n    $validated = $request->validate($this->rules());\n\n    // Check authorization\n    if (! $request->user()->can('view', $user)) {\n        return response()->json(['error' => 'Unauthorized'], 403);\n    }\n\n    // Sanitize sensitive data\n    $data = $user->analytics($validated);\n    unset($data['internal_notes'], $data['admin_flags']);\n\n    return response()->json(['data' => $data]);\n}",{"id":3918,"title":3919,"titles":3920,"content":224,"level":188},"/docs/mcp/getters#common-getter-patterns","Common Getter Patterns",[136],{"id":3922,"title":3923,"titles":3924,"content":3925,"level":199},"/docs/mcp/getters#analytics-getter","Analytics Getter",[136,3919],"class UserActivityAnalyticsGetter extends Getter\n{\n    public string $description = 'Get detailed activity analytics for a user';\n\n    public function rules(): array\n    {\n        return [\n            'period' => ['required', 'in:7d,30d,90d'],\n            'include_trends' => ['boolean'],\n        ];\n    }\n\n    public function handle(Request $request, User $user)\n    {\n        $validated = $request->validate($this->rules());\n\n        return response()->json([\n            'data' => $user->activityAnalytics($validated),\n        ]);\n    }\n}",{"id":3927,"title":3928,"titles":3929,"content":3930,"level":199},"/docs/mcp/getters#financial-getter","Financial Getter",[136,3919],"class UserRevenueBreakdownGetter extends Getter\n{\n    public string $description = 'Get revenue breakdown for a specific user by product and region';\n\n    public function rules(): array\n    {\n        return [\n            'start_date' => ['required', 'date'],\n            'end_date' => ['required', 'date', 'after:start_date'],\n            'currency' => ['string', 'in:USD,EUR,GBP'],\n        ];\n    }\n\n    public function handle(Request $request, User $user)\n    {\n        $validated = $request->validate($this->rules());\n\n        return response()->json([\n            'data' => $user->revenueBreakdown($validated),\n        ]);\n    }\n}",{"id":3932,"title":3933,"titles":3934,"content":3935,"level":199},"/docs/mcp/getters#subscription-status-getter","Subscription Status Getter",[136,3919],"class SubscriptionStatusGetter extends Getter\n{\n    public string $description = 'Check user subscription status and health';\n\n    public function handle(Request $request, User $user)\n    {\n        return response()->json([\n            'data' => [\n                'subscription_active' => $user->hasActiveSubscription(),\n                'payment_method_valid' => $user->hasValidPaymentMethod(),\n                'next_billing_date' => $user->nextBillingDate(),\n                'status' => $user->subscriptionStatus(),\n            ],\n        ]);\n    }\n} This MCP getter system allows AI agents to retrieve single-resource data intelligently while maintaining security, validation, and proper parameter requirements. Unlike Actions, Getters are focused specifically on retrieving data for individual resources, making them ideal for analytics, status checks, and detailed information retrieval. html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":141,"title":140,"titles":3937,"content":3938,"level":182},[],"Learn how Restify converts Laravel validation rules into JSON Schema for MCP agents using JsonSchemaFromRulesAction, with wildcard array and type support. JsonSchemaFromRulesAction converts Laravel validation rules into Illuminate\\\\JsonSchema types that are ready to surface to Model Context Protocol (MCP) agents. Restify runs this converter automatically for MCP actions and getters, but you can invoke it yourself whenever you need a programmatic JSON Schema.",{"id":3940,"title":354,"titles":3941,"content":3942,"level":188},"/docs/mcp/json-schema-converter#how-it-works",[140],"Rule parsing – JsonSchemaFromRulesAction normalizes the rules through Laravel's validator (validator()->make()->getRules()) so aliases like string|min:3 are expanded exactly as the framework would interpret them.Rule to schema mapping – The heavy lifting happens inside SchemaAttributes where more than one hundred validate{Rule} methods translate individual validation rules into schema metadata (type, min/max, enum, textual description, etc.).Array wildcards – After each attribute is processed, processWildcardRules() inspects attribute.* definitions and attaches their schema to the parent array items, ensuring nested collection rules (for example tags.*) are represented correctly.",{"id":3944,"title":3945,"titles":3946,"content":3947,"level":188},"/docs/mcp/json-schema-converter#using-the-helper-function","Using the Helper Function",[140],"The simplest way to convert validation rules to JSON Schema is using the mcpSchema() helper function: $rules = [\n    'event_date' => ['required', 'date', 'before:2025-12-31'],\n    'age' => ['required', 'integer', 'min:18'],\n];\n\n$types = mcpSchema($rules);\n$payload = array_map(fn ($type) => $type->toArray(), $types); This helper automatically resolves the JsonSchema instance and runs the converter, returning an array of Type instances keyed by attribute name.",{"id":3949,"title":3950,"titles":3951,"content":3952,"level":188},"/docs/mcp/json-schema-converter#using-the-converter-directly","Using the Converter Directly",[140],"If you need more control, you can instantiate the converter directly: use Binaryk\\LaravelRestify\\MCP\\Actions\\JsonSchemaFromRulesAction;\nuse Illuminate\\JsonSchema\\JsonSchemaTypeFactory;\n\n$converter = new JsonSchemaFromRulesAction();\n$schema = new JsonSchemaTypeFactory();\n\n$rules = [\n    'event_date' => ['required', 'date', 'before:2025-12-31'],\n    'age' => ['required', 'integer', 'min:18'],\n];\n\n$types = $converter($schema, $rules);\n$payload = array_map(fn ($type) => $type->toArray(), $types); $types is an array of Type instances keyed by attribute. Turn them into JSON (as above) or compose them inside an ObjectType if you need an OpenAPI-style structure with a required array.",{"id":3954,"title":1831,"titles":3955,"content":224,"level":188},"/docs/mcp/json-schema-converter#practical-examples",[140],{"id":3957,"title":3958,"titles":3959,"content":3960,"level":199},"/docs/mcp/json-schema-converter#event-scheduling","Event Scheduling",[140,1831],"$rules = [\n    'event_date' => ['required', 'date', 'before:2025-12-31'],\n]; Result: {\n  \"event_date\": {\n    \"type\": \"string\",\n    \"description\": \"This is a date attribute. Must be before: 2025-12-31\",\n    \"required\": true\n  }\n}",{"id":3962,"title":3963,"titles":3964,"content":3965,"level":199},"/docs/mcp/json-schema-converter#numeric-bounds-for-mcp-actions","Numeric Bounds for MCP Actions",[140,1831],"$rules = [\n    'quantity' => ['required', 'integer', 'min:1', 'max:100'],\n]; Result: {\n  \"quantity\": {\n    \"type\": \"integer\",\n    \"minimum\": 1,\n    \"maximum\": 100,\n    \"description\": \"Maximum value: 100\"\n  }\n} SchemaAttributes::validateMin() and validateMax() detect the integer type and translate the Laravel limits into JSON Schema minimum / maximum constraints so MCP agents know the allowed range.",{"id":3967,"title":3968,"titles":3969,"content":3970,"level":199},"/docs/mcp/json-schema-converter#array-items-with-wildcard-rules","Array Items with Wildcard Rules",[140,1831],"$rules = [\n    'tags' => ['required', 'array', 'min:1', 'max:5'],\n    'tags.*' => ['string', 'max:20'],\n]; Result: {\n  \"tags\": {\n    \"type\": \"array\",\n    \"minItems\": 1,\n    \"maxItems\": 5,\n    \"description\": \"Maximum items: 5\",\n    \"items\": {\n      \"type\": \"string\",\n      \"maxLength\": 20,\n      \"description\": \"Maximum length: 20 characters\"\n    }\n  }\n} processWildcardRules() notices the tags.* rule, builds a temporary schema for the wildcard attribute, and attaches it to tags as the items definition. The same mechanism powers nested MCP action parameters such as cart line items or recipient lists.",{"id":3972,"title":3973,"titles":3974,"content":3975,"level":188},"/docs/mcp/json-schema-converter#tips","Tips",[140],"The converter gracefully handles Closure-based rules and custom Rule objects by defaulting to string types with descriptive text, so MCP consumers still receive a hint about the constraint.If you need bespoke descriptions, add your own rule-specific method to a class that uses SchemaAttributes, or post-process the returned Type objects before serializing them.When exposing the schema publicly, consider wrapping the output in an ObjectType ($schema->object()->properties($types)) to emit a JSON Schema document that lists required properties explicitly. With just a handful of rule definitions you can now expose accurate JSON Schema payloads to MCP agents, front-end builders, or any consumer expecting machine-readable validation metadata. html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":145,"title":144,"titles":3977,"content":3978,"level":182},[],"Learn how to expose Laravel Restify actions to AI agents via MCP, configure action scopes, generate JSON Schema from validation rules, and control visibility. Laravel Restify Actions can be exposed to AI agents through the Model Context Protocol (MCP), allowing intelligent automation of complex operations. This page covers how to configure actions for MCP integration and optimize them for AI consumption.",{"id":3980,"title":3981,"titles":3982,"content":3983,"level":188},"/docs/mcp/actions#enabling-mcp-actions","Enabling MCP Actions",[144],"To expose actions to the MCP server, you must enable them in your repository by overriding the mcpAllowsActions() method: use Binaryk\\LaravelRestify\\Traits\\HasMcpTools;\n\n#[Model(Post::class)]\nclass PostRepository extends Repository\n{\n    use HasMcpTools;\n\n    // Enable action tools for AI agents\n    public function mcpAllowsActions(): bool\n    {\n        return true; // Default: false\n    }\n\n    public function actions(RestifyRequest $request): array\n    {\n        return [\n            PublishPostsAction::new(),\n            ArchivePostsAction::new(),\n            SendNewsletterAction::new()->standalone(),\n        ];\n    }\n} Once enabled, all actions defined in the actions() method will be automatically exposed as MCP tools to AI agents.",{"id":3985,"title":3986,"titles":3987,"content":3988,"level":188},"/docs/mcp/actions#action-descriptions-for-ai-agents","Action Descriptions for AI Agents",[144],"Providing clear descriptions helps AI agents understand when and how to use your actions. You can customize action descriptions using the $description property or by overriding the description() method.",{"id":3990,"title":3828,"titles":3991,"content":3992,"level":199},"/docs/mcp/actions#using-the-description-property",[144,3986],"use Binaryk\\LaravelRestify\\Actions\\Action;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\ActionRequest;\nuse Illuminate\\Support\\Collection;\n\nclass PublishPostsAction extends Action\n{\n    public string $description = 'Publish selected blog posts and optionally notify subscribers via email. Posts must be in draft status to be published.';\n\n    public function handle(ActionRequest $request, Collection $posts)\n    {\n        // Action implementation\n    }\n}",{"id":3994,"title":3554,"titles":3995,"content":3996,"level":199},"/docs/mcp/actions#using-the-description-method",[144,3986],"For dynamic descriptions based on context: class PublishPostsAction extends Action\n{\n    public function description(RestifyRequest $request): string\n    {\n        $userRole = $request->user()?->role;\n\n        if ($userRole === 'editor') {\n            return 'Publish selected posts after editorial review. Sends notifications to authors and subscribers.';\n        }\n\n        return 'Publish selected posts immediately. Requires publish permissions.';\n    }\n\n    //...\n}",{"id":3998,"title":857,"titles":3999,"content":4000,"level":188},"/docs/mcp/actions#validation-rules-for-ai-schema",[144],"The rules() method is crucial for MCP integration. Restify automatically converts your Laravel validation rules into JSON Schema that AI agents understand and use for parameter validation. class PublishPostsAction extends Action\n{\n    public string $description = 'Publish posts with optional scheduling and notification settings';\n\n    public function rules(): array\n    {\n        return [\n            'publish_immediately' => ['boolean'],\n            'scheduled_at' => ['nullable', 'date', 'after:now'],\n            'notify_subscribers' => ['boolean'],\n            'notification_message' => ['nullable', 'string', 'max:500'],\n            'notify_authors' => ['boolean'],\n        ];\n    }\n\n    public function handle(ActionRequest $request, Collection $posts)\n    {\n        $request->validate($this->rules());\n\n        // Action implementation\n    }\n} The AI agent will automatically receive a JSON Schema with: publish_immediately: boolean (optional)scheduled_at: date string (optional, must be in the future)notify_subscribers: boolean (optional)notification_message: string (optional, max 500 characters)notify_authors: boolean (optional)",{"id":4002,"title":4003,"titles":4004,"content":4005,"level":188},"/docs/mcp/actions#action-scopes-and-mcp-enforcement","Action Scopes and MCP Enforcement",[144],"Actions can have different scopes that determine what parameters AI agents must provide. The scope affects the generated MCP tool schema and validation.",{"id":4007,"title":4008,"titles":4009,"content":4010,"level":199},"/docs/mcp/actions#index-actions-multiple-resources","Index Actions (Multiple Resources)",[144,4003],"Index actions operate on multiple resources. When using onlyOnIndex(), AI agents are required to provide a resources parameter containing an array of model IDs: class ArchivePostsAction extends Action\n{\n    public string $description = 'Archive multiple posts and remove them from public view';\n\n    public function handle(ActionRequest $request, Collection $posts)\n    {\n        $posts->each->archive();\n    }\n}\n\n// In PostRepository\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        ArchivePostsAction::new()->onlyOnIndex(),\n    ];\n} Generated MCP Tool Schema: {\n    \"name\": \"posts-archive-posts-action-tool\",\n    \"description\": \"Execute Archive Posts action on Post records in the posts repository.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"resources\": {\n                \"type\": \"array\",\n                \"items\": {\n                    \"type\": \"string\",\n                    \"description\": \"The ID of the resource Post to perform the action on.\",\n                    \"required\": true\n                },\n                \"title\": \"resources\",\n                \"description\": \"The ids of the resources Post to perform the action on. Use string 'all' to select all resources.\",\n                \"required\": true\n            }\n        },\n        \"required\": [\"resources\"]\n    }\n} AI Agent Usage: // Must provide resources array\n{\n    \"resources\": [\"1\", \"2\", \"3\"]\n}\n\n// Or use \"all\" to apply to all resources\n{\n    \"resources\": \"all\"\n}",{"id":4012,"title":4013,"titles":4014,"content":4015,"level":199},"/docs/mcp/actions#show-actions-single-resource","Show Actions (Single Resource)",[144,4003],"Show actions operate on a single resource. When using onlyOnShow(), AI agents are required to provide an id parameter: class PublishPostAction extends Action\n{\n    public string $description = 'Publish a specific post and notify its author';\n\n    public function handle(ActionRequest $request, Post $post)\n    {\n        $post->publish();\n        $post->author->notify(new PostPublishedNotification($post));\n    }\n}\n\n// In PostRepository\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        PublishPostAction::new()->onlyOnShow(),\n    ];\n} Generated MCP Tool Schema: {\n    \"name\": \"posts-publish-post-action-tool\",\n    \"description\": \"Execute Publish Post action on a specific Post record in the posts repository.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"id\": {\n                \"type\": \"string\",\n                \"title\": \"id\",\n                \"description\": \"The ID of the resource to perform the action on.\",\n                \"required\": true\n            }\n        },\n        \"required\": [\"id\"]\n    }\n} AI Agent Usage: // Must provide the id\n{\n    \"id\": \"123\"\n}",{"id":4017,"title":4018,"titles":4019,"content":4020,"level":199},"/docs/mcp/actions#default-actions-index-behavior","Default Actions (Index Behavior)",[144,4003],"If you don't specify a scope (no onlyOnShow() or onlyOnIndex()), the action defaults to index behavior and still requires the resources parameter: class NotifyAuthorsAction extends Action\n{\n    public string $description = 'Send notification to authors of selected posts';\n\n    public function handle(ActionRequest $request, Collection $posts)\n    {\n        // Notify authors\n    }\n}\n\n// In PostRepository - no scope modifier\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        NotifyAuthorsAction::new(), // Defaults to index behavior\n    ];\n} AI Agent Usage: // Still requires resources array (index behavior)\n{\n    \"resources\": [\"1\", \"2\", \"3\"]\n}",{"id":4022,"title":4023,"titles":4024,"content":4025,"level":199},"/docs/mcp/actions#standalone-actions-no-resources-required","Standalone Actions (No Resources Required)",[144,4003],"Standalone actions don't operate on specific resources. When using standalone(), AI agents are not required to provide any resource identifiers: class GenerateMonthlyReportAction extends Action\n{\n    public bool $standalone = true;\n\n    public string $description = 'Generate a monthly report of all blog activity and email it to administrators';\n\n    public function rules(): array\n    {\n        return [\n            'month' => ['required', 'integer', 'between:1,12'],\n            'year' => ['required', 'integer', 'min:2020'],\n            'include_drafts' => ['boolean'],\n        ];\n    }\n\n    public function handle(ActionRequest $request)\n    {\n        // Generate report logic - no models required\n        $report = Report::generate($request->validated());\n\n        return response()->json(['report_url' => $report->url]);\n    }\n}\n\n// In PostRepository\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        GenerateMonthlyReportAction::new()->standalone(),\n    ];\n} Generated MCP Tool Schema: {\n    \"name\": \"posts-generate-monthly-report-action-tool\",\n    \"description\": \"Execute Generate Monthly Report action (standalone - no models required) in the posts repository.\",\n    \"inputSchema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"month\": {\n                \"type\": \"integer\",\n                \"minimum\": 1,\n                \"maximum\": 12,\n                \"required\": true\n            },\n            \"year\": {\n                \"type\": \"integer\",\n                \"minimum\": 2020,\n                \"required\": true\n            },\n            \"include_drafts\": {\n                \"type\": \"boolean\"\n            }\n        },\n        \"required\": [\"month\", \"year\"]\n    }\n} AI Agent Usage: // No id or resources required - only action parameters\n{\n    \"month\": 10,\n    \"year\": 2025,\n    \"include_drafts\": true\n}",{"id":4027,"title":4028,"titles":4029,"content":4030,"level":188},"/docs/mcp/actions#scope-comparison-table","Scope Comparison Table",[144],"ScopeMethodRequired ParameterParameter TypeUse CaseIndexonlyOnIndex()resourcesarray or \"all\"Operate on multiple resourcesShowonlyOnShow()idstringOperate on a single resourceDefault(none)resourcesarray or \"all\"Same as index (optimized default)Standalonestandalone()(none)-No resources needed",{"id":4032,"title":4033,"titles":4034,"content":4035,"level":188},"/docs/mcp/actions#complete-action-example","Complete Action Example",[144],"Here's a comprehensive example showing all aspects of MCP-optimized actions: use Binaryk\\LaravelRestify\\Actions\\Action;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\ActionRequest;\nuse Illuminate\\Support\\Collection;\n\nclass SchedulePostPublicationAction extends Action\n{\n    // Clear description for AI agents\n    public string $description = 'Schedule posts for future publication with optional notification settings. Posts will be automatically published at the specified date and time.';\n\n    // Comprehensive validation rules converted to JSON Schema\n    public function rules(): array\n    {\n        return [\n            'publish_at' => ['required', 'date', 'after:now'],\n            'timezone' => ['nullable', 'string', 'timezone'],\n            'notify_subscribers' => ['boolean'],\n            'send_social_media' => ['boolean'],\n            'social_platforms' => ['nullable', 'array'],\n            'social_platforms.*' => ['string', 'in:twitter,facebook,linkedin'],\n            'custom_message' => ['nullable', 'string', 'max:280'],\n        ];\n    }\n\n    public function handle(ActionRequest $request, Collection $posts)\n    {\n        $validated = $request->validate($this->rules());\n\n        foreach ($posts as $post) {\n            $post->update([\n                'scheduled_at' => $validated['publish_at'],\n                'timezone' => $validated['timezone'] ?? 'UTC',\n            ]);\n\n            if ($validated['notify_subscribers'] ?? false) {\n                $post->scheduleSubscriberNotification();\n            }\n\n            if ($validated['send_social_media'] ?? false) {\n                $post->scheduleSocialMediaPosts(\n                    $validated['social_platforms'] ?? [],\n                    $validated['custom_message'] ?? null\n                );\n            }\n        }\n\n        return ok('Posts scheduled successfully');\n    }\n}\n\n// In PostRepository\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        SchedulePostPublicationAction::new()->onlyOnIndex(),\n    ];\n}",{"id":4037,"title":4038,"titles":4039,"content":4040,"level":188},"/docs/mcp/actions#hiding-actions-from-mcp","Hiding Actions from MCP",[144],"You can selectively hide actions from MCP while keeping them available for regular API requests. This is useful when you want certain actions to be manually triggered by users but not accessible to AI agents.",{"id":4042,"title":3866,"titles":4043,"content":4044,"level":199},"/docs/mcp/actions#using-hidefrommcp",[144,4038],"public function actions(RestifyRequest $request): array\n{\n    return [\n        // Available to both API and MCP\n        PublishPostsAction::new()->onlyOnIndex(),\n\n        // Available only to API requests, hidden from AI agents\n        MarkInvoiceAsPaidRestifyAction::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n\n        // Conditionally hide based on environment\n        DeletePostsAction::new()\n            ->hideFromMcp(app()->environment('production'))\n            ->onlyOnIndex(),\n\n        // Available only in development for AI testing\n        TestDataGeneratorAction::new()\n            ->hideFromMcp(! app()->environment('local'))\n            ->standalone(),\n    ];\n}",{"id":4046,"title":4047,"titles":4048,"content":4049,"level":199},"/docs/mcp/actions#use-cases-for-hiding-actions","Use Cases for Hiding Actions",[144,4038],"1. High-Risk Operations public function actions(RestifyRequest $request): array\n{\n    return [\n        // Never expose deletion to AI agents\n        PermanentlyDeleteUsersAction::new()\n            ->hideFromMcp()\n            ->onlyOnIndex(),\n\n        // Never expose financial operations to AI\n        ProcessRefundAction::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n    ];\n} 2. Manual-Only Workflows public function actions(RestifyRequest $request): array\n{\n    return [\n        // Require human approval, not AI\n        ApproveComplianceDocumentAction::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n\n        // Manual verification required\n        VerifyIdentityAction::new()\n            ->hideFromMcp()\n            ->onlyOnShow(),\n    ];\n} 3. Environment-Specific Exposure public function actions(RestifyRequest $request): array\n{\n    return [\n        // Only expose to AI in development\n        SeedTestDataAction::new()\n            ->hideFromMcp(! app()->environment('local'))\n            ->standalone(),\n\n        // Hide from AI in production\n        ExperimentalFeatureAction::new()\n            ->hideFromMcp(app()->environment('production'))\n            ->onlyOnIndex(),\n    ];\n}",{"id":4051,"title":3876,"titles":4052,"content":4053,"level":199},"/docs/mcp/actions#combining-with-other-modifiers",[144,4038],"The hideFromMcp() method works seamlessly with other action modifiers: public function actions(RestifyRequest $request): array\n{\n    return [\n        MarkInvoiceAsPaidRestifyAction::new()\n            ->hideFromMcp()                    // Hide from AI agents\n            ->onlyOnShow()                     // Only for single invoices\n            ->canSee(fn() => $request->user()->can('manage-invoices')),  // Authorization\n\n        BulkDeleteAction::new()\n            ->hideFromMcp(app()->environment('production'))  // Conditional hiding\n            ->onlyOnIndex()\n            ->canSee(fn() => $request->user()->isAdmin()),\n    ];\n} Hidden actions are completely excluded from MCP tool discovery. AI agents will not see them in the available tools list and cannot execute them, even if they somehow know the action URI key.",{"id":4055,"title":4056,"titles":4057,"content":4058,"level":188},"/docs/mcp/actions#conditional-action-visibility","Conditional Action Visibility",[144],"You can control which actions are visible to AI agents based on permissions: class PostRepository extends Repository\n{\n    use HasMcpTools;\n\n    public function mcpAllowsActions(): bool\n    {\n        // Only enable actions for specific roles\n        return request()->user()?->hasRole(['admin', 'editor']) ?? false;\n    }\n\n    public function actions(RestifyRequest $request): array\n    {\n        return [\n            // Only admins can delete\n            DeletePostsAction::new()\n                ->canSee(fn() => $request->user()?->hasRole('admin')),\n\n            // Editors and admins can publish\n            PublishPostsAction::new()\n                ->canSee(fn() => $request->user()?->hasRole(['admin', 'editor'])),\n\n            // Everyone with action access can archive\n            ArchivePostsAction::new(),\n\n            // Hidden from AI but available to API\n            CriticalOperationAction::new()\n                ->hideFromMcp(),\n        ];\n    }\n}",{"id":4060,"title":4061,"titles":4062,"content":224,"level":188},"/docs/mcp/actions#best-practices-for-mcp-actions","Best Practices for MCP Actions",[144],{"id":4064,"title":3890,"titles":4065,"content":4066,"level":199},"/docs/mcp/actions#_1-write-clear-descriptions",[144,4061],"// Good: Specific, explains purpose and behavior\npublic string $description = 'Archive selected posts and move them to the archive section. Archived posts are hidden from public view but remain searchable by administrators.';\n\n// Poor: Too vague\npublic string $description = 'Archive posts';",{"id":4068,"title":3895,"titles":4069,"content":4070,"level":199},"/docs/mcp/actions#_2-use-comprehensive-validation-rules",[144,4061],"// Good: Detailed validation that generates rich JSON Schema\npublic function rules(): array\n{\n    return [\n        'priority' => ['required', 'integer', 'between:1,5'],\n        'category' => ['required', 'string', 'in:urgent,normal,low'],\n        'assign_to' => ['nullable', 'exists:users,id'],\n        'due_date' => ['nullable', 'date', 'after:today'],\n        'tags' => ['array', 'max:5'],\n        'tags.*' => ['string', 'max:50'],\n    ];\n}\n\n// Poor: Minimal validation\npublic function rules(): array\n{\n    return [\n        'priority' => ['integer'],\n    ];\n}",{"id":4072,"title":4073,"titles":4074,"content":4075,"level":199},"/docs/mcp/actions#_3-choose-the-right-scope","3. Choose the Right Scope",[144,4061],"// Use standalone for actions that don't need resources\nGenerateReportAction::new()->standalone()\n\n// Use onlyOnShow for single-resource operations\nPublishPostAction::new()->onlyOnShow()\n\n// Use onlyOnIndex for bulk operations\nArchivePostsAction::new()->onlyOnIndex()",{"id":4077,"title":4078,"titles":4079,"content":4080,"level":199},"/docs/mcp/actions#_4-provide-helpful-error-messages","4. Provide Helpful Error Messages",[144,4061],"public function handle(ActionRequest $request, Collection $posts)\n{\n    $validated = $request->validate($this->rules());\n\n    if ($posts->isEmpty()) {\n        return error('No posts selected for archiving', 422);\n    }\n\n    if ($posts->count() > 100) {\n        return error('Cannot archive more than 100 posts at once. Please select fewer posts.', 422);\n    }\n\n    // Action implementation\n}",{"id":4082,"title":3410,"titles":4083,"content":4084,"level":188},"/docs/mcp/actions#security-considerations",[144],"Validate all input: Always validate using $request->validate($this->rules())Check permissions: Use authorization gates or policiesLimit bulk operations: Set maximum limits for index actionsAudit actions: Log important actions for security tracking public function handle(ActionRequest $request, Collection $posts)\n{\n    // Validate input\n    $validated = $request->validate($this->rules());\n\n    // Check authorization\n    if (! $request->user()->can('publish', Post::class)) {\n        return error('Unauthorized', 403);\n    }\n\n    // Limit bulk operations\n    if ($posts->count() > 50) {\n        return error('Cannot publish more than 50 posts at once', 422);\n    }\n\n    // Log the action\n    activity()\n        ->causedBy($request->user())\n        ->withProperties(['post_ids' => $posts->pluck('id')])\n        ->log('bulk_publish_posts');\n\n    // Perform action\n    $posts->each->publish();\n\n    return ok('Posts published successfully');\n}",{"id":4086,"title":4087,"titles":4088,"content":4089,"level":188},"/docs/mcp/actions#real-world-examples","Real-World Examples",[144],"Here are complete, production-ready examples of each action type with their generated MCP schemas.",{"id":4091,"title":4092,"titles":4093,"content":4094,"level":199},"/docs/mcp/actions#example-1-show-action-mark-invoice-as-paid","Example 1: Show Action - Mark Invoice as Paid",[144,4087],"Action Class: \u003C?php\n\nnamespace App\\Restify\\Invoices\\Actions;\n\nuse App\\Domains\\Invoices\\Models\\Invoice;\nuse App\\Domains\\Permissions\\Enum\\Permissions;\nuse Binaryk\\LaravelRestify\\Actions\\Action;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\ActionRequest;\nuse Illuminate\\Http\\JsonResponse;\n\nclass MarkInvoiceAsPaidRestifyAction extends Action\n{\n    public bool $showOnShow = true;\n\n    public static $uriKey = 'mark-as-paid';\n\n    public string $description = 'Mark a specific invoice as paid by recording the payment date. This updates the invoice status and triggers payment confirmation emails.';\n\n    public function handle(ActionRequest $request, Invoice $invoice): JsonResponse\n    {\n        abort_if(\n            ! $request->user()->can(Permissions::manageInvoices),\n            403,\n            __('Unauthorized to perform this action!')\n        );\n\n        $request->validate($this->rules());\n\n        $invoice->markAsPaid($request->date('date'));\n\n        return data($invoice->fresh());\n    }\n\n    public function rules(): array\n    {\n        return [\n            'date' => ['required', 'date', 'before_or_equal:today'],\n        ];\n    }\n} Repository Registration: public function actions(RestifyRequest $request): array\n{\n    return [\n        MarkInvoiceAsPaidRestifyAction::make()->onlyOnShow(),\n    ];\n} Generated MCP Tool Schema: {\n    \"name\": \"invoices-mark-as-paid-action-tool\",\n    \"title\": \"Mark Invoice As Paid Restify Action\",\n    \"description\": \"Mark a specific invoice as paid by recording the payment date. This updates the invoice status and triggers payment confirmation emails.\",\n    \"inputSchema\": {\n        \"properties\": {\n            \"date\": {\n                \"description\": \"This field is required. Must be a valid date. Must be a date before or equal to today.\",\n                \"type\": \"string\"\n            },\n            \"id\": {\n                \"title\": \"id\",\n                \"description\": \"The ID of the resource to perform the action on.\",\n                \"type\": \"string\"\n            }\n        },\n        \"type\": \"object\",\n        \"required\": [\n            \"date\",\n            \"id\"\n        ]\n    }\n} AI Agent Usage: // AI must provide both the invoice ID and payment date\n{\n    \"id\": \"123\",\n    \"date\": \"2025-10-05\"\n} Same Action with ->onlyOnIndex() Scope: If the same MarkInvoiceAsPaidRestifyAction was registered with ->onlyOnIndex() instead: // Different repository registration\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        MarkInvoiceAsPaidRestifyAction::make()->onlyOnIndex(), // Changed from onlyOnShow()\n    ];\n} Generated MCP Tool Schema Changes: {\n    \"name\": \"invoices-mark-as-paid-action-tool\",\n    \"title\": \"Mark Invoice As Paid Restify Action\",\n    \"description\": \"Execute Mark Invoice As Paid Restify Action action on Invoice records in the invoices repository.\",\n    \"inputSchema\": {\n        \"properties\": {\n            \"date\": {\n                \"description\": \"This field is required\",\n                \"type\": \"string\"\n            },\n            \"resources\": {\n                \"title\": \"resources\",\n                \"description\": \"The ids of the resources Invoice to perform the action on. Use string 'all' to select all resources.\",\n                \"items\": {\n                    \"description\": \"The ID of the resource Invoice to perform the action on.\",\n                    \"type\": \"string\"\n                },\n                \"type\": \"array\"\n            }\n        },\n        \"type\": \"object\",\n        \"required\": [\n            \"date\",\n            \"resources\"\n        ]\n    }\n} Key Differences: Description changes from \"on a specific Invoice record\" to \"on Invoice records\" (plural)id parameter replaced with resources arrayAI must provide multiple invoice IDs instead of a single ID AI Agent Usage with Index Scope: // AI must provide resources array instead of single id\n{\n    \"resources\": [\"123\", \"124\", \"125\"],\n    \"date\": \"2025-10-05\"\n}\n\n// Or mark all invoices as paid\n{\n    \"resources\": \"all\",\n    \"date\": \"2025-10-05\"\n} The same action class can be registered with different scopes depending on your use case. Use `onlyOnShow()` when you want AI agents to mark individual invoices as paid, or `onlyOnIndex()` when you want to enable bulk payment marking.",{"id":4096,"title":4097,"titles":4098,"content":4099,"level":199},"/docs/mcp/actions#example-2-index-action-archive-multiple-posts","Example 2: Index Action - Archive Multiple Posts",[144,4087],"Action Class: \u003C?php\n\nnamespace App\\Restify\\Posts\\Actions;\n\nuse App\\Models\\Post;\nuse Binaryk\\LaravelRestify\\Actions\\Action;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\ActionRequest;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Support\\Collection;\n\nclass ArchivePostsRestifyAction extends Action\n{\n    public bool $showOnIndex = true;\n\n    public static $uriKey = 'archive-posts';\n\n    public string $description = 'Archive multiple blog posts and optionally notify their authors. Archived posts are hidden from public view but remain accessible to administrators.';\n\n    public function handle(ActionRequest $request, Collection $posts): JsonResponse\n    {\n        abort_if(\n            ! $request->user()->can('archive', Post::class),\n            403,\n            __('Unauthorized to archive posts!')\n        );\n\n        $validated = $request->validate($this->rules());\n\n        $archivedCount = 0;\n\n        foreach ($posts as $post) {\n            $post->update([\n                'archived_at' => now(),\n                'archive_reason' => $validated['reason'] ?? null,\n            ]);\n\n            if ($validated['notify_authors'] ?? false) {\n                $post->author->notify(new PostArchivedNotification($post, $validated['reason'] ?? null));\n            }\n\n            $archivedCount++;\n        }\n\n        return data([\n            'message' => \"Successfully archived {$archivedCount} posts\",\n            'archived_count' => $archivedCount,\n        ]);\n    }\n\n    public function rules(): array\n    {\n        return [\n            'reason' => ['nullable', 'string', 'max:500'],\n            'notify_authors' => ['boolean'],\n        ];\n    }\n} Repository Registration: public function actions(RestifyRequest $request): array\n{\n    return [\n        ArchivePostsRestifyAction::make()->onlyOnIndex(),\n    ];\n} Generated MCP Tool Schema: {\n    \"name\": \"posts-archive-posts-action-tool\",\n    \"title\": \"Archive Posts Restify Action\",\n    \"description\": \"Archive multiple blog posts and optionally notify their authors. Archived posts are hidden from public view but remain accessible to administrators.\",\n    \"inputSchema\": {\n        \"properties\": {\n            \"reason\": {\n                \"description\": \"Must not be greater than 500 characters.\",\n                \"type\": \"string\",\n                \"maxLength\": 500\n            },\n            \"notify_authors\": {\n                \"type\": \"boolean\"\n            },\n            \"resources\": {\n                \"type\": \"array\",\n                \"items\": {\n                    \"type\": \"string\",\n                    \"description\": \"The ID of the resource Post to perform the action on.\",\n                    \"required\": true\n                },\n                \"title\": \"resources\",\n                \"description\": \"The ids of the resources Post to perform the action on. Use string 'all' to select all resources.\",\n                \"required\": true\n            }\n        },\n        \"type\": \"object\",\n        \"required\": [\n            \"resources\"\n        ]\n    }\n} AI Agent Usage: // AI must provide the resources array\n{\n    \"resources\": [\"45\", \"67\", \"89\"],\n    \"reason\": \"Content outdated and needs revision\",\n    \"notify_authors\": true\n}\n\n// Or archive all posts matching filters\n{\n    \"resources\": \"all\",\n    \"reason\": \"Seasonal content cleanup\",\n    \"notify_authors\": false\n}",{"id":4101,"title":4102,"titles":4103,"content":4104,"level":199},"/docs/mcp/actions#example-3-standalone-action-generate-monthly-report","Example 3: Standalone Action - Generate Monthly Report",[144,4087],"Action Class: \u003C?php\n\nnamespace App\\Restify\\Reports\\Actions;\n\nuse App\\Services\\ReportGenerator;\nuse Binaryk\\LaravelRestify\\Actions\\Action;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\ActionRequest;\nuse Illuminate\\Http\\JsonResponse;\n\nclass GenerateMonthlyReportRestifyAction extends Action\n{\n    public bool $standalone = true;\n\n    public static $uriKey = 'generate-monthly-report';\n\n    public string $description = 'Generate a comprehensive monthly report including sales metrics, user growth, revenue analysis, and top performing content. The report is generated in PDF format and emailed to specified recipients.';\n\n    public function handle(ActionRequest $request): JsonResponse\n    {\n        abort_if(\n            ! $request->user()->hasRole('admin'),\n            403,\n            __('Only administrators can generate reports!')\n        );\n\n        $validated = $request->validate($this->rules());\n\n        $report = app(ReportGenerator::class)->generateMonthlyReport(\n            month: $validated['month'],\n            year: $validated['year'],\n            includeDrafts: $validated['include_drafts'] ?? false,\n            sections: $validated['sections'] ?? ['sales', 'users', 'content']\n        );\n\n        if ($validated['send_email'] ?? false) {\n            foreach ($validated['recipients'] as $email) {\n                $report->sendTo($email);\n            }\n        }\n\n        return data([\n            'report_id' => $report->id,\n            'report_url' => $report->download_url,\n            'generated_at' => $report->created_at,\n            'email_sent' => $validated['send_email'] ?? false,\n        ]);\n    }\n\n    public function rules(): array\n    {\n        return [\n            'month' => ['required', 'integer', 'between:1,12'],\n            'year' => ['required', 'integer', 'min:2020', 'max:2030'],\n            'include_drafts' => ['boolean'],\n            'sections' => ['array'],\n            'sections.*' => ['string', 'in:sales,users,content,revenue,analytics'],\n            'send_email' => ['boolean'],\n            'recipients' => ['required_if:send_email,true', 'array'],\n            'recipients.*' => ['email'],\n        ];\n    }\n} Repository Registration: // Can be registered in any repository since it's standalone\npublic function actions(RestifyRequest $request): array\n{\n    return [\n        GenerateMonthlyReportRestifyAction::make()->standalone(),\n    ];\n} Generated MCP Tool Schema: {\n    \"name\": \"posts-generate-monthly-report-action-tool\",\n    \"title\": \"Generate Monthly Report Restify Action\",\n    \"description\": \"Generate a comprehensive monthly report including sales metrics, user growth, revenue analysis, and top performing content. The report is generated in PDF format and emailed to specified recipients.\",\n    \"inputSchema\": {\n        \"properties\": {\n            \"month\": {\n                \"type\": \"integer\",\n                \"minimum\": 1,\n                \"maximum\": 12,\n                \"description\": \"This field is required. Must be between 1 and 12.\"\n            },\n            \"year\": {\n                \"type\": \"integer\",\n                \"minimum\": 2020,\n                \"maximum\": 2030,\n                \"description\": \"This field is required. Must be at least 2020. Must not be greater than 2030.\"\n            },\n            \"include_drafts\": {\n                \"type\": \"boolean\"\n            },\n            \"sections\": {\n                \"type\": \"array\",\n                \"items\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"sales\", \"users\", \"content\", \"revenue\", \"analytics\"]\n                }\n            },\n            \"send_email\": {\n                \"type\": \"boolean\"\n            },\n            \"recipients\": {\n                \"type\": \"array\",\n                \"description\": \"This field is required when send email is true.\",\n                \"items\": {\n                    \"type\": \"string\",\n                    \"format\": \"email\",\n                    \"description\": \"Must be a valid email address.\"\n                }\n            }\n        },\n        \"type\": \"object\",\n        \"required\": [\n            \"month\",\n            \"year\"\n        ]\n    }\n} AI Agent Usage: // No id or resources required - only action parameters\n{\n    \"month\": 9,\n    \"year\": 2025,\n    \"include_drafts\": false,\n    \"sections\": [\"sales\", \"users\", \"revenue\"],\n    \"send_email\": true,\n    \"recipients\": [\"admin@example.com\", \"manager@example.com\"]\n}",{"id":4106,"title":4107,"titles":4108,"content":4109,"level":199},"/docs/mcp/actions#example-4-default-action-index-behavior","Example 4: Default Action (Index Behavior)",[144,4087],"Action Class: \u003C?php\n\nnamespace App\\Restify\\Users\\Actions;\n\nuse App\\Notifications\\BulkNotification;\nuse Binaryk\\LaravelRestify\\Actions\\Action;\nuse Binaryk\\LaravelRestify\\Http\\Requests\\ActionRequest;\nuse Illuminate\\Http\\JsonResponse;\nuse Illuminate\\Support\\Collection;\n\nclass SendBulkNotificationRestifyAction extends Action\n{\n    // No scope modifier - defaults to index behavior\n\n    public static $uriKey = 'send-notification';\n\n    public string $description = 'Send a custom notification to multiple users via email, SMS, or push notification. Allows personalized messages with template variables.';\n\n    public function handle(ActionRequest $request, Collection $users): JsonResponse\n    {\n        $validated = $request->validate($this->rules());\n\n        $sentCount = 0;\n\n        foreach ($users as $user) {\n            $user->notify(new BulkNotification(\n                message: $validated['message'],\n                channels: $validated['channels'],\n                priority: $validated['priority'] ?? 'normal'\n            ));\n\n            $sentCount++;\n        }\n\n        return data([\n            'message' => \"Notification sent to {$sentCount} users\",\n            'sent_count' => $sentCount,\n        ]);\n    }\n\n    public function rules(): array\n    {\n        return [\n            'message' => ['required', 'string', 'max:1000'],\n            'channels' => ['required', 'array'],\n            'channels.*' => ['string', 'in:email,sms,push'],\n            'priority' => ['string', 'in:low,normal,high,urgent'],\n        ];\n    }\n} AI Agent Usage: // Still requires resources array (default index behavior)\n{\n    \"resources\": [\"12\", \"34\", \"56\"],\n    \"message\": \"Important system update scheduled for tonight\",\n    \"channels\": [\"email\", \"push\"],\n    \"priority\": \"high\"\n} These examples demonstrate how different action scopes work in practice and how the MCP system generates appropriate schemas for AI agents to interact with your Laravel Restify actions.",{"id":4111,"title":4112,"titles":4113,"content":4114,"level":188},"/docs/mcp/actions#summary","Summary",[144],"This MCP action system allows AI agents to perform complex operations on your data while maintaining security, validation, and proper scoping constraints. html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .su27w, html code.shiki .su27w{--shiki-light:#916B53;--shiki-default:#916B53;--shiki-dark:#916B53}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":154,"title":153,"titles":4116,"content":4117,"level":182},[],"Boost Laravel Restify API speed with policy caching, index meta toggling, and repository index caching to handle large datasets efficiently.",{"id":4119,"title":4120,"titles":4121,"content":4122,"level":188},"/docs/performance/performance#policy-caching","Policy Caching",[153],"When loading a large number of models, Restify will check each policy method as show or allowRestify (including for all relations) before serializing them. In order to improve performance, Restify caches the policies. You simply have to enable the caching by setting the restify.cache.policies.enabled property to true in the restify.php configuration file: 'cache' => [\n    'policies' => [\n        'enabled' => true,\n        'ttl' => 5 * 60, // seconds\n    ],\n], The caching is tight to the current authenticated user so if another user is logged in, the cache will be hydrated for the new user once again. Restify allows individual caching at the policy level with specific configurations. To enable this, a contract Cacheable must be implemented at the policy level, which enforces the use of the cache() method. class PostPolicy implements Cacheable\n{\n    public function cache(): ?CarbonInterface\n    {\n        return now()->addMinutes();\n    } The cache method is expected to return a CarbonInterface or null. If null is returned, the current policy will NOT cached.",{"id":4124,"title":4125,"titles":4126,"content":4127,"level":188},"/docs/performance/performance#disable-index-meta","Disable index meta",[153],"Index meta are policy information related to what actions are allowed on a resource for a specific user. However, if you don't need this information, you can disable the index meta by setting the restify.repositories.serialize_index_meta property to false in the restify.php configuration file: 'repositories' => [\n    'serialize_index_meta' => false,\n    \n    'serialize_show_meta' => true,\n], This will give your application a boost, especially when loading a large amount of resources or relations.",{"id":4129,"title":4130,"titles":4131,"content":4132,"level":188},"/docs/performance/performance#index-friendly-search-and-sort","Index-friendly search and sort",[153],"Restify ships with two opt-in optimizations that let database indexes do their work: Sort by relation can emit a LEFT JOIN instead of a per-row correlated subquery. See JOIN strategy for sort-by-relation.Searchable case modes let each column declare whether to wrap the column in UPPER() / LOWER(), transform only the search value, or stay strictly raw. See Per-field case modes. Both are off by default and rolled out per-column or per-config — strict backward compatibility.",{"id":4134,"title":4135,"titles":4136,"content":4137,"level":188},"/docs/performance/performance#repository-index-caching","Repository Index Caching",[153],"Laravel Restify provides powerful caching for repository index requests to dramatically improve performance for expensive queries with filters, searches, sorts, and pagination. This feature can reduce response times by orders of magnitude for complex API endpoints.",{"id":4139,"title":4140,"titles":4141,"content":4142,"level":199},"/docs/performance/performance#quick-setup","Quick Setup",[153,4135],"Enable repository caching in your .env file: # Enable repository index caching\nRESTIFY_REPOSITORY_CACHE_ENABLED=true\n\n# Cache TTL in seconds (default: 300 = 5 minutes)\nRESTIFY_REPOSITORY_CACHE_TTL=300\n\n# Optional: Specify cache store\nRESTIFY_REPOSITORY_CACHE_STORE=redis That's it! Your repository index endpoints will now be cached automatically.",{"id":4144,"title":379,"titles":4145,"content":4146,"level":199},"/docs/performance/performance#configuration",[153,4135],"All caching options are available in config/restify.php: 'repositories' => [\n    'cache' => [\n        // Enable or disable caching globally\n        'enabled' => env('RESTIFY_REPOSITORY_CACHE_ENABLED', false),\n        \n        // Default TTL in seconds\n        'ttl' => env('RESTIFY_REPOSITORY_CACHE_TTL', 300),\n        \n        // Cache store to use (null = default)\n        'store' => env('RESTIFY_REPOSITORY_CACHE_STORE'),\n        \n        // Skip caching for authenticated users\n        'skip_authenticated' => false,\n        \n        // Enable in test environment (disabled by default)\n        'enable_in_tests' => false,\n        \n        // Cache tags for efficient invalidation\n        'tags' => ['restify', 'repositories'],\n    ],\n],",{"id":4148,"title":4149,"titles":4150,"content":4151,"level":199},"/docs/performance/performance#repository-specific-configuration","Repository-Specific Configuration",[153,4135],"Customize caching per repository: class PostRepository extends Repository\n{\n    // Disable caching for this repository\n    public static bool $cacheEnabled = false;\n    \n    // Custom TTL (10 minutes)\n    public static int $cacheTtl = 600;\n    \n    // Use specific cache store\n    public static ?string $cacheStore = 'redis';\n    \n    // Custom cache tags\n    public static array $cacheTags = ['posts', 'content'];\n}",{"id":4153,"title":4154,"titles":4155,"content":4156,"level":199},"/docs/performance/performance#smart-cache-keys","Smart Cache Keys",[153,4135],"The system generates unique cache keys based on: Repository type (users, posts, etc.)Request parameters (search, filters, sorting, pagination)User context (for authorization-sensitive data)Model timestamps (for automatic invalidation) Example cache key: restify:repository:posts:index:7ed77bab35bfc8f3fd4da03ffdde2370:user_1:v_1756392802",{"id":4158,"title":4159,"titles":4160,"content":4161,"level":199},"/docs/performance/performance#cache-store-compatibility","Cache Store Compatibility",[153,4135],"Full Support (with cache tags): ✅ Redis Store✅ Memcached Store✅ Array Store (testing) Basic Support (TTL-based): ✅ Database Store✅ File Store The system automatically detects cache store capabilities and gracefully falls back when advanced features aren't supported.",{"id":4163,"title":4164,"titles":4165,"content":4166,"level":199},"/docs/performance/performance#automatic-cache-invalidation","Automatic Cache Invalidation",[153,4135],"Cache is automatically cleared when: // Model events trigger cache clearing\n$post = Post::create([...]);  // Clears post cache\n$post->update([...]);         // Clears post cache\n$post->delete();              // Clears post cache",{"id":4168,"title":4169,"titles":4170,"content":4171,"level":199},"/docs/performance/performance#manual-cache-management","Manual Cache Management",[153,4135],"// Clear cache for specific repository\nPostRepository::clearCache();\n\n// Configure caching at runtime\nPostRepository::enableCache();\nPostRepository::disableCache();\nPostRepository::cacheTtl(600); // 10 minutes\nPostRepository::cacheTags(['posts', 'content']);",{"id":4173,"title":4174,"titles":4175,"content":4176,"level":199},"/docs/performance/performance#performance-impact","Performance Impact",[153,4135],"Caching provides dramatic performance improvements: Complex filters: 50-90% faster response timesLarge datasets: Reduces database load significantlyPagination: Instant subsequent page loadsSearch queries: Eliminates expensive LIKE operationsAuthorization: Caches user-specific policy checks",{"id":4178,"title":4179,"titles":4180,"content":4181,"level":199},"/docs/performance/performance#test-environment-safety","Test Environment Safety",[153,4135],"Caching is disabled by default in tests to prevent test isolation issues: // Tests automatically have caching disabled\nclass MyTest extends TestCase {\n    public function test_something() {\n        // Caching is off - no cache pollution between tests\n    }\n}\n\n// Enable caching for specific tests\nclass CacheTest extends TestCase {\n    public function test_with_cache() {\n        $this->enableRepositoryCache();\n        // Now caching is enabled for this test\n    }\n}",{"id":4183,"title":1617,"titles":4184,"content":4185,"level":199},"/docs/performance/performance#best-practices",[153,4135],"Production Focused: Enable caching in production where it matters mostMonitor TTL: Set appropriate cache TTL based on data update frequencyUse Redis: Redis provides the best caching experience with full tag supportTag Strategy: Use meaningful cache tags for efficient bulk invalidationAuthorization-Aware: Caching respects user permissions automatically",{"id":4187,"title":4188,"titles":4189,"content":4190,"level":199},"/docs/performance/performance#example-usage","Example Usage",[153,4135],"// Before caching: 500ms response time\nGET /api/restify/posts?search=laravel&sort=created_at&page=2\n\n// After caching: 20ms response time (25x faster!)\nGET /api/restify/posts?search=laravel&sort=created_at&page=2\n\n// Different parameters = different cache\nGET /api/restify/posts?search=php&sort=title&page=1 // New cache entry\n\n// Cache respects user context\n// User A and User B get different cached results based on permissions This caching system provides a significant performance boost with zero code changes required - simply enable it in configuration and enjoy faster API responses! html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"id":158,"title":157,"titles":4192,"content":4193,"level":182},[],"Laravel Restify can auto-suggest AI-powered fixes for exceptions in debug mode by extending RestifyHandler and connecting your OpenAI API key. Inspired by the Marcel's Article.",{"id":4195,"title":4196,"titles":4197,"content":4198,"level":188},"/docs/performance/solutions#generate-solution","Generate solution",[157],"Restify can generate an AI based solution to your problem. In order to enable that you need to extend the App\\Exceptions\\Handler with the Binaryk\\LaravelRestify\\Exceptions\\RestifyHandler: use Binaryk\\LaravelRestify\\Exceptions\\RestifyHandler;\nuse Throwable;\n\nclass Handler extends RestifyHandler\n{\n    //...\n} This feature is only enabled when the `app.debug` is set to `true`. This feature is using the openai-php/laravel, you should also publish the config file: php artisan vendor:publish --provider=\"OpenAI\\Laravel\\ServiceProvider\" and set the OPENAI_API_KEY in the .env file. The OpenAI key can be obtained from here. Now the solution to your problems will automatically appear in the response: {\n    \"restify-solution\": \"Line 67 in DocumentRepository.php file has an error because the method `resolveUsingFullPath()` is not defined. The code should look like this:\\n```\\n->resolveUsingTemporaryUrl($request->boolean('temporary'))\\n```\\n\",\n    \"message\": \"Call to undefined method Binaryk\\\\LaravelRestify\\\\Fields\\\\File::resolveUsingFullPath()\",\n    \"exception\": \"Error\",\n    \"file\": \"/Users/eduardlupacescu/Sites/binarcode/erp/app/Restify/DocumentRepository.php\",\n    \"line\": 67,\n    \"trace\": [\n...\n}",{"id":4200,"title":4201,"titles":4202,"content":4203,"level":188},"/docs/performance/solutions#disable-solution","Disable solution",[157],"If you want to disable the solution feature you can set the restify.ai_solution to false in the config/restify.php file so Restify will not call the OpenAI API even you extended the exception handler. This might be useful in automated tests or other environments: 'ai_solutions' => true, html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"id":167,"title":166,"titles":4205,"content":4206,"level":182},[],"Restify Boost is a Laravel Restify extension that adds an MCP server, letting AI agents generate repositories, actions, getters, and query API docs.",{"id":4208,"title":4209,"titles":4210,"content":4211,"level":188},"/docs/boost/boost#mcp-server-for-laravel-restify-developers","MCP Server for Laravel Restify Developers",[166],"Restify Boost provides a dedicated MCP server for developers that enhances the development experience when working with Laravel Restify APIs. Repository: https://github.com/BinarCode/laravel-restify-boost",{"id":4213,"title":4214,"titles":4215,"content":4216,"level":199},"/docs/boost/boost#developer-mcp-server-features","Developer MCP Server Features",[166,4209],"The Laravel Restify MCP server provides AI agents with powerful development tools: 📚 Documentation Access: Query Laravel Restify documentation directly from your AI agent🏗️ Repository Generation: Create new repositories with proper structure and conventions⚡ Action Creation: Generate custom actions for your API resources with validation and best practices🔍 Getter Development: Build custom getters for specialized data retrieval operations💡 Code Examples: Get contextual code examples and implementation guidance🎯 Best Practices: Receive Laravel Restify best practices and architectural guidance",{"id":4218,"title":4219,"titles":4220,"content":224,"level":199},"/docs/boost/boost#installation-setup","Installation & Setup",[166,4209],{"id":4222,"title":4223,"titles":4224,"content":4225,"level":834},"/docs/boost/boost#install-the-mcp-server","Install the MCP Server",[166,4209,4219],"composer require --dev binarcode/laravel-restify-boost",{"id":4227,"title":4228,"titles":4229,"content":4230,"level":834},"/docs/boost/boost#configure-ai-agents","Configure AI Agents",[166,4209,4219],"Configure your AI agent (Claude Desktop, Cursor, etc.) to use the MCP server: {\n  \"mcpServers\": {\n    \"laravel-restify\": {\n      \"command\": \"php\",\n      \"args\": [\n        \"artisan\",\n        \"restify-boost:start\"\n      ]\n    }\n  }\n}",{"id":4232,"title":4233,"titles":4234,"content":4235,"level":834},"/docs/boost/boost#usage-examples","Usage Examples",[166,4209,4219],"Once configured, your AI agent can help with: Creating Repositories: AI: Create a PostRepository with title, content, and author fields Generating Actions: AI: Create a PublishPostAction that validates publish dates and notifies subscribers Building Getters: AI: Generate a PostAnalyticsGetter that returns engagement metrics for date ranges Documentation Queries: AI: How do I implement field validation in Laravel Restify?\nAI: Show me examples of custom repository authorization html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":176,"title":175,"titles":4237,"content":4238,"level":182},[],"Learn how to test Laravel Restify repositories using partialMock to stub actions and assert JSON API responses in your PHPUnit test cases. class ExampleTest extends TestCase\n{\n    public function testBasicTest()\n    {\n        UserRepository::partialMock()\n            ->shouldReceive('index')\n            ->andReturn(['data' => [],]);\n\n        $this->withHeader('Accept', 'application/json')\n            ->get('/api/restify/users')\n            ->assertJsonStructure([\n                'response' => 'data',\n            ])->assertOk();\n    }\n} So you can use the partialMock to get the partial mock instance of the repository, and then perform actions or expectations over it. html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":6,"title":10,"titles":4240,"content":4241,"level":182},[],"Transform Laravel Eloquent models into JSON:API endpoints and MCP servers automatically. Complete Laravel API framework with authentication, filtering, and AI agent integration. Build once, serve everywhere - Create APIs that work seamlessly for developers, web applications, mobile apps, and AI agents like Claude Desktop.",{"id":4243,"title":4244,"titles":4245,"content":4246,"level":188},"/docs#key-features","Key Features",[10],"JSON:API endpoints from Laravel modelsMCP Server generation for AI agentsAuthentication with Laravel SanctumSearch & Filtering with powerful query capabilitiesGraphQL schema generationAuthorization using Laravel policies",{"id":4248,"title":4249,"titles":4250,"content":4251,"level":188},"/docs#templates","Templates",[10],"Need a head start? Check out our Restify Templates for ready-made API starter kits.",{"id":4253,"title":4254,"titles":4255,"content":4256,"level":188},"/docs#playground","Playground",[10],"You can find a playground in the Restify Demo GitHub repository.",{"id":4258,"title":4259,"titles":4260,"content":4261,"level":188},"/docs#videos","Videos",[10],"You can find the full course here with many useful features and insights on how Laravel Restify works. Note that the videos are from a previous version of Restify and may not reflect the latest features.",{"id":4263,"title":166,"body":4264,"description":4206,"extension":4573,"meta":4574,"navigation":4576,"path":167,"seo":4577,"stem":168,"__hash__":4578},"content/docs/8.boost/1.boost.md",{"type":4265,"value":4266,"toc":4567},"minimark",[4267,4271,4280,4292,4296,4299,4339,4342,4346,4373,4376,4379,4511,4514,4517,4522,4530,4535,4541,4546,4552,4557,4563],[4268,4269,4209],"h2",{"id":4270},"mcp-server-for-laravel-restify-developers",[4272,4273,4274,4275,4279],"p",{},"Restify Boost provides a dedicated ",[4276,4277,4278],"strong",{},"MCP server for developers"," that enhances the development experience when working with Laravel Restify APIs.",[4272,4281,4282,4285,4286],{},[4276,4283,4284],{},"Repository",": ",[4287,4288,4289],"a",{"href":4289,"rel":4290},"https://github.com/BinarCode/laravel-restify-boost",[4291],"nofollow",[4293,4294,4214],"h3",{"id":4295},"developer-mcp-server-features",[4272,4297,4298],{},"The Laravel Restify MCP server provides AI agents with powerful development tools:",[4300,4301,4302,4309,4315,4321,4327,4333],"ul",{},[4303,4304,4305,4308],"li",{},[4276,4306,4307],{},"📚 Documentation Access",": Query Laravel Restify documentation directly from your AI agent",[4303,4310,4311,4314],{},[4276,4312,4313],{},"🏗️ Repository Generation",": Create new repositories with proper structure and conventions",[4303,4316,4317,4320],{},[4276,4318,4319],{},"⚡ Action Creation",": Generate custom actions for your API resources with validation and best practices",[4303,4322,4323,4326],{},[4276,4324,4325],{},"🔍 Getter Development",": Build custom getters for specialized data retrieval operations",[4303,4328,4329,4332],{},[4276,4330,4331],{},"💡 Code Examples",": Get contextual code examples and implementation guidance",[4303,4334,4335,4338],{},[4276,4336,4337],{},"🎯 Best Practices",": Receive Laravel Restify best practices and architectural guidance",[4293,4340,4219],{"id":4341},"installation-setup",[4343,4344,4223],"h4",{"id":4345},"install-the-mcp-server",[4347,4348,4352],"pre",{"className":4349,"code":4350,"language":4351,"meta":224,"style":224},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","composer require --dev binarcode/laravel-restify-boost\n","bash",[4353,4354,4355],"code",{"__ignoreMap":224},[4356,4357,4359,4363,4367,4370],"span",{"class":4358,"line":182},"line",[4356,4360,4362],{"class":4361},"sBMFI","composer",[4356,4364,4366],{"class":4365},"sfazB"," require",[4356,4368,4369],{"class":4365}," --dev",[4356,4371,4372],{"class":4365}," binarcode/laravel-restify-boost\n",[4343,4374,4228],{"id":4375},"configure-ai-agents",[4272,4377,4378],{},"Configure your AI agent (Claude Desktop, Cursor, etc.) to use the MCP server:",[4347,4380,4384],{"className":4381,"code":4382,"language":4383,"meta":224,"style":224},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"mcpServers\": {\n    \"laravel-restify\": {\n      \"command\": \"php\",\n      \"args\": [\n        \"artisan\",\n        \"restify-boost:start\"\n      ]\n    }\n  }\n}\n","json",[4353,4385,4386,4392,4410,4424,4448,4463,4476,4487,4493,4499,4505],{"__ignoreMap":224},[4356,4387,4388],{"class":4358,"line":182},[4356,4389,4391],{"class":4390},"sMK4o","{\n",[4356,4393,4394,4397,4401,4404,4407],{"class":4358,"line":188},[4356,4395,4396],{"class":4390},"  \"",[4356,4398,4400],{"class":4399},"spNyl","mcpServers",[4356,4402,4403],{"class":4390},"\"",[4356,4405,4406],{"class":4390},":",[4356,4408,4409],{"class":4390}," {\n",[4356,4411,4412,4415,4418,4420,4422],{"class":4358,"line":199},[4356,4413,4414],{"class":4390},"    \"",[4356,4416,4417],{"class":4361},"laravel-restify",[4356,4419,4403],{"class":4390},[4356,4421,4406],{"class":4390},[4356,4423,4409],{"class":4390},[4356,4425,4426,4429,4433,4435,4437,4440,4443,4445],{"class":4358,"line":834},[4356,4427,4428],{"class":4390},"      \"",[4356,4430,4432],{"class":4431},"sbssI","command",[4356,4434,4403],{"class":4390},[4356,4436,4406],{"class":4390},[4356,4438,4439],{"class":4390}," \"",[4356,4441,4442],{"class":4365},"php",[4356,4444,4403],{"class":4390},[4356,4446,4447],{"class":4390},",\n",[4356,4449,4451,4453,4456,4458,4460],{"class":4358,"line":4450},5,[4356,4452,4428],{"class":4390},[4356,4454,4455],{"class":4431},"args",[4356,4457,4403],{"class":4390},[4356,4459,4406],{"class":4390},[4356,4461,4462],{"class":4390}," [\n",[4356,4464,4466,4469,4472,4474],{"class":4358,"line":4465},6,[4356,4467,4468],{"class":4390},"        \"",[4356,4470,4471],{"class":4365},"artisan",[4356,4473,4403],{"class":4390},[4356,4475,4447],{"class":4390},[4356,4477,4479,4481,4484],{"class":4358,"line":4478},7,[4356,4480,4468],{"class":4390},[4356,4482,4483],{"class":4365},"restify-boost:start",[4356,4485,4486],{"class":4390},"\"\n",[4356,4488,4490],{"class":4358,"line":4489},8,[4356,4491,4492],{"class":4390},"      ]\n",[4356,4494,4496],{"class":4358,"line":4495},9,[4356,4497,4498],{"class":4390},"    }\n",[4356,4500,4502],{"class":4358,"line":4501},10,[4356,4503,4504],{"class":4390},"  }\n",[4356,4506,4508],{"class":4358,"line":4507},11,[4356,4509,4510],{"class":4390},"}\n",[4343,4512,4233],{"id":4513},"usage-examples",[4272,4515,4516],{},"Once configured, your AI agent can help with:",[4272,4518,4519],{},[4276,4520,4521],{},"Creating Repositories:",[4347,4523,4528],{"className":4524,"code":4526,"language":4527},[4525],"language-text","AI: Create a PostRepository with title, content, and author fields\n","text",[4353,4529,4526],{"__ignoreMap":224},[4272,4531,4532],{},[4276,4533,4534],{},"Generating Actions:",[4347,4536,4539],{"className":4537,"code":4538,"language":4527},[4525],"AI: Create a PublishPostAction that validates publish dates and notifies subscribers\n",[4353,4540,4538],{"__ignoreMap":224},[4272,4542,4543],{},[4276,4544,4545],{},"Building Getters:",[4347,4547,4550],{"className":4548,"code":4549,"language":4527},[4525],"AI: Generate a PostAnalyticsGetter that returns engagement metrics for date ranges\n",[4353,4551,4549],{"__ignoreMap":224},[4272,4553,4554],{},[4276,4555,4556],{},"Documentation Queries:",[4347,4558,4561],{"className":4559,"code":4560,"language":4527},[4525],"AI: How do I implement field validation in Laravel Restify?\nAI: Show me examples of custom repository authorization\n",[4353,4562,4560],{"__ignoreMap":224},[4564,4565,4566],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":224,"searchDepth":188,"depth":188,"links":4568},[4569],{"id":4270,"depth":188,"text":4209,"children":4570},[4571,4572],{"id":4295,"depth":199,"text":4214},{"id":4341,"depth":199,"text":4219},"md",{"category":4575},"Extensions",true,{"title":166,"description":4206},"fRGdssLYHJ_CmEpjkv_4ZUcKAyZfm09eU_hDQl7mwU4",[4580],{"title":5,"path":6,"stem":7,"children":4581,"page":-1},[4582,4583,4586,4591,4604,4609,4613,4621,4625,4628],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":4584,"page":21},[4585],{"title":18,"path":19,"stem":20},{"title":23,"path":24,"stem":25,"children":4587,"page":21},[4588,4589,4590],{"title":28,"path":29,"stem":30},{"title":32,"path":33,"stem":34},{"title":36,"path":37,"stem":38},{"title":40,"path":41,"stem":42,"children":4592,"page":21},[4593,4594,4595,4596,4597,4598,4599,4600,4601,4602,4603],{"title":45,"path":46,"stem":47},{"title":49,"path":50,"stem":51},{"title":53,"path":54,"stem":55},{"title":57,"path":58,"stem":59},{"title":61,"path":62,"stem":63},{"title":65,"path":66,"stem":67},{"title":69,"path":70,"stem":71},{"title":73,"path":74,"stem":75},{"title":77,"path":78,"stem":79},{"title":81,"path":82,"stem":83},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":4605,"page":21},[4606,4607,4608],{"title":94,"path":95,"stem":96},{"title":98,"path":99,"stem":100},{"title":102,"path":103,"stem":104},{"title":106,"path":107,"stem":108,"children":4610,"page":21},[4611,4612],{"title":111,"path":112,"stem":113},{"title":115,"path":116,"stem":117},{"title":119,"path":120,"stem":121,"children":4614,"page":21},[4615,4616,4617,4618,4619,4620],{"title":124,"path":125,"stem":126},{"title":128,"path":129,"stem":130},{"title":132,"path":133,"stem":134},{"title":136,"path":137,"stem":138},{"title":140,"path":141,"stem":142},{"title":144,"path":145,"stem":146},{"title":148,"path":149,"stem":150,"children":4622,"page":21},[4623,4624],{"title":153,"path":154,"stem":155},{"title":157,"path":158,"stem":159},{"title":161,"path":162,"stem":163,"children":4626,"page":21},[4627],{"title":166,"path":167,"stem":168},{"title":170,"path":171,"stem":172,"children":4629,"page":21},[4630],{"title":175,"path":176,"stem":177},[4632,4633],{"title":157,"path":158,"stem":159,"children":-1},{"title":175,"path":176,"stem":177,"children":-1},1781862712000]