{"id":18782413,"url":"https://github.com/atk4/api","last_synced_at":"2025-04-13T12:08:23.792Z","repository":{"id":55942190,"uuid":"107142772","full_name":"atk4/api","owner":"atk4","description":"Implementation of RestAPI for Agile Data","archived":false,"fork":false,"pushed_at":"2020-12-13T14:26:57.000Z","size":110,"stargazers_count":14,"open_issues_count":9,"forks_count":12,"subscribers_count":9,"default_branch":"develop","last_synced_at":"2025-04-05T00:06:10.036Z","etag":null,"topics":["agile","api","atk4","data","php","rest"],"latest_commit_sha":null,"homepage":"https://agiletoolkit.org/","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/atk4.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-16T14:58:16.000Z","updated_at":"2023-11-22T10:39:32.000Z","dependencies_parsed_at":"2022-08-15T10:00:38.818Z","dependency_job_id":null,"html_url":"https://github.com/atk4/api","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atk4%2Fapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atk4%2Fapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atk4%2Fapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atk4%2Fapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/atk4","download_url":"https://codeload.github.com/atk4/api/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248631681,"owners_count":21136562,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agile","api","atk4","data","php","rest"],"created_at":"2024-11-07T20:35:55.037Z","updated_at":"2025-04-13T12:08:23.767Z","avatar_url":"https://github.com/atk4.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Agile API Framework\n\n[![Build Status](https://travis-ci.org/atk4/api.png?branch=develop)](https://travis-ci.org/atk4/api)\n[![StyleCI](https://styleci.io/repos/107142772/shield)](https://styleci.io/repos/107142772)\n[![codecov](https://codecov.io/gh/atk4/api/branch/develop/graph/badge.svg)](https://codecov.io/gh/atk4/api)\n[![Code Climate](https://codeclimate.com/github/atk4/api/badges/gpa.svg)](https://codeclimate.com/github/atk4/api)\n[![Issue Count](https://codeclimate.com/github/atk4/api/badges/issue_count.svg)](https://codeclimate.com/github/atk4/api/issues)\n\n[![License](https://poser.pugx.org/atk4/api/license)](https://packagist.org/packages/atk4/api)\n[![GitHub release](https://img.shields.io/github/release/atk4/api.svg?maxAge=2592000)](https://packagist.org/packages/atk4/api)\n\nEnd-to-end implementation for your RESTful API and RPC. Provides a very simple means for you to define API end-points for the application that already uses [Agile Data](https://github.com/atk4/data).\n\n## 1. Simple To Use\n\nAgile API strives to be very simple and work out of the box. Below is a minimal code to get your basic API going, put that into `v1.php` file then invoke `composer require Atk4/Api` :\n\n``` php\ninclude 'vendor/autoload.php';\n\n$api = new \\Atk4\\Api\\Api();\n\n// Simple handling of GET request through a callback.\n$api-\u003eget('/ping', function() {\n   return 'Pong';\n});\n\n// Methods can accept arguments, and everything is type-safe.\n$api-\u003eget('/hello/:name', function ($name) {\n    return \"Hello, $name\";\n});\n```\n\n## 2. Agile Data Integration\n\n[Agile Data](https://github.com/atk4/data) is a data persistence framework. In simple terms, you can use Agile Data to create your business models (entities) and interact with the database. Agile API is designed to be a perfect integration if you are have already defined classes and persistence in Agile Data. Next code assumes you have `Model Country` and `Persistence $db`:\n\n``` php\n$api-\u003erest('/countries', new Country($db));\n```\n\nThis creates a standard standard-compliant RESTful interface for interfacing the client model:\n\n-   `GET /countries` responds with list of all Country records from $db.\n-   `POST /countries` adds a new Country reading data from Form data or JSON in POST body.\n-   `GET /countries/123` loads client with specified ID.\n-   `PATCH /countries/123` with some Form data or JSON will update existing Country.\n-   `DELETE /countries/123` will delete a record.\n\nThrough Agile UI you may add conditions, limits and more. Also second argument can be a call-back:\n\n``` php\n$api-\u003erest('/countries', function() use($db) {\n  $c = new Country($db));\n  $c-\u003eaddCondition('is_eu', true);\n  $c-\u003esetLimit(20);\n  return $c;\n});\n```\n\nField types, data conversions, validation and hooks can all be defined through Agile Data, making the API layer very transparent and simple. If you attempt to load non-existant record, API will respond with 404. Other errors will be properly mapped to the API codes or fallback to 500.\n\n\n\n# Work in progress\n\nAgile UI is still a work in progress. This readme will be further updated to reflect a current features.\n\n\n\n## Planned Features\n\nAgile API is in development but the following features are planned:\n\n-   [x] Simple to use.\n\n\n-   [x] Model routing. Provide end-points by associating them with models.\n-   [ ] Global authentication. Provide authentication strategy for entire framework.\n-   [ ] Support for rate limits. Per-account, per-IP counters which can be stored in MemCache or Redis.\n-   [ ] Deep logging, integrated with data persistence. Not only stores the request, but what data was affected inside persistence.\n-   [ ] Support for API UNDO. Neutralize effect of API call had on your backend.\n\n### Simple to use\n\nTo set up your API, simply create new RestAPI class instance and define routes. You can enable versioning by creating \"v1\" folder and placing `index.php` in that folder. Some things work and we do not want to re-invent them!\n\n``` php\nrequire 'vendor/autoload.php';\n$app = new \\Atk4\\Api\\Api();\n\n$db = \\Atk4\\Data\\Persistence::connect($DSN);\n\n// Lets set our index page\n$app-\u003eget('/', function() {\n    return 'This worked!';\n});\n\n// Getting access to POST data\n$app-\u003epost('/stats/:id', function($id, $data) {\n   return ['Received POST', 'id'=\u003e$id, 'post_data'=\u003e$data]\n});\n```\n\nCalling methods such as `get()`, `post()` with a function call-back will register them and if URL matches a pattern, all the matching callbacks will be executed, that is, until some of them will present a return value.\n\nExecution will occur as soon as the match is confirmed (to help with error display).\n\nTechnically this allows multiple call-backs to be matched:\n\n``` php\n$app-\u003eget('/:method', function($method) {\n    // do something\n});\n\n$app-\u003eget('/ping', function() {\n    return 'pong';\n});\n```\n\nNote, that some popular PHP API frameworks (like Slim) use {name} for matching parameters, however rest of IT industry prefers using \":name\" instead. We will use industry pattern matching, but will try to also support {$foo}, although it does look too similar to Agile UI template tags.\n\nI think that the methods can be cleverly made to match the rules too:\n\n``` php\nfunction get($route, $action) {\n    if ($_SERVER['REQUEST_METHOD'] == 'GET') {\n    \treturn $this-\u003ematch($route, $action);\n    }\n}\n```\n\nA useful note about `match` is that it can be used without action and will return `true`/`false`.\n\n``` php\nif ($app-\u003ematch('/misc/**')) {\n    // .. execute logic for requests starting with /misc/...\n} else {\n    // .. other logic\n}\n```\n\n### Model Routing\n\nMethod `rest()` implements a standard Restful API end-point dedicated to a model. There are two ways to use it:\n\n``` php\n$app-\u003erest('/clients', new Client($db));\n```\n\nThis would simple enable all the necessary operations for accessing the model, in particular:\n\n-   GET /clients - listing all clients\n-   GET /clients/:id - get specific client data\n-   POST /clients - create new client\n-   PUT /clients/:id - same as patch\n-   PATCH /clients/:id - load, update specified fields only, save\n-   DELETE /clients/:id - delete specific client record\n\nYou can also specify a different field if you don't want to use primary key:\n\n``` php\n$app-\u003erest('/country/:iso_name');\n```\n\nAgile Data offers powerful ways of traversing references, and the above approach can also utilize:\n\n``` php\n$app-\u003erest('/clients/:id/orders/::Orders:id', new Client($db));\n```\n\nThis would create new route for URLs such as `/clients/123/orders/395`. The model for the client with id 123 would be loaded first, then ref('Orders') would be executed. The rest of the logic is similar to before.\n\nThis gives us option to perform deep traversal too:\n\n``` php\n$app-\u003erest('/clients/:id/order_payments/::Orders::Payments:id', new Client($db));\n```\n\nThis would load the Client, perform ref('Orders')-\u003eref('Payments'). Finally, the \"id\" is optional:\n\n```php\n$app-\u003erest('/client/:/order_payments/::Orders::Payments', new Client($db));\n```\n\nSometimes you would want to have even more control, so you can use:\n\n``` php\n$app-\u003erest('/client/:id/invoices-due', function($id) use($db) {\n    $client = new Client($db);\n    $client-\u003eload($id);\n    return $client-\u003eref('Invoices')-\u003eaddCondition('status', 'due');\n});\n```\n\nMethod `rest()` builds on top of methods `put()`, `get()`, `post()` and others. Third argument to method `rest()` can specify array with options.\n\n## Auth\n\nOur API supports various authentication methods. Some of them are built-in and 3rd party extensions can also be used.\n\nLets look at the very basic user/password authentication.\n\n``` php\n// Enable user/password authentication. Field values are optional\n$app-\u003euserAuth('/**', new User($db));\n```\n\nYou can place the authentication method strategically, and it will protect all the further routes but not the ones above it. Also you can use a custom route if you wish to only protect some portion of your API.\n\nThe method AUTH will look for HTTP_AUTH headers and will respond with 405 code if user record cannot be loaded with a corresponding user/password combination.\n\nAfter user authentication is performed, `$app-\u003euser` will exist:\n\n``` php\n$app-\u003eauthUser('/**', new User($db));\n$app-\u003erest('/notifications', $app-\u003euser-\u003eref('Notifications'));\n```\n\n### Rate Limit\n\nRate Limit support will limit number of requests which user (or IP) can make. It's easy to set it up:\n\n``` php\n$app-\u003eauthUser('/**', new User($db));\n\n$limit = new \\Atk4\\Api\\Limit($db);\n$limit-\u003eaddCondition('user_id', $app-\u003euser-\u003eid);\n\n$app-\u003eget('/limits', function() use ($limit){\n    return $limit;\n});\n\n$app-\u003erateLimit('/**', $limit, 10);  // 10 requests per minute\n\n$app-\u003erest('/notifications', $app-\u003euser-\u003eref('Notifications'));\n```\n\nIt's preferable to use rate limits with persistence such as Redis or Memcache:\n\n``` php\n$cache = \\Atk4\\Data\\Persistence\\MemCache($conn);\n$limit = new \\Atk4\\Api\\Limit($cache);\n```\n\n### Deep logging\n\nAgile Data already supports audit log, but with Agile API you can compliment that even further:\n\n``` php\n$audit_id = $app-\u003eauditLog(\n  '/**',\n  new \\Atk4\\Audit\\Controller(\n    new \\Atk4\\Audit\\Model\\AuditLog($db)\n  )\n);\n```\n\nThis would create a log entry per invocation and use it for all the subsequent changes inside data persistence.\n\nNote that the `$audit_id` produced by the above function can also be used for UNDO action:\n\n``` php\n$app-\u003eauditLog-\u003eload($audit_id)-\u003eundo();\n```\n\nwhich would also reverse all the changes done on the persistence layer.\n\n### Error Logging\n\nSimilarly to Agile UI, the application for API will catch exceptions raised.\n\n``` php\n$app-\u003e?\n```\n\n### System support and global scoping\n\nAgile Data supports global scoping, so you can add additional hook that would affect creation of all the models and add some further conditioning. That's useful based off the Auth response:\n\n``` php\n$user_id = $app-\u003eauthUser('/**', new User($db));\n\n$db-\u003eaddHook('afterAdd', function($o, $e) use ($user_id) {\n    if ($e-\u003ehasElement('user_id')) {\n        $e-\u003eaddCondition('user_id', $user_id);\n    }\n})\n\n```\n\n### Mapping to file-system\n\n``` php\n$app-\u003emap('/:resource/**', function(resource) use($app) {\n\n    // convert user-credit to UserCredit\n    $class = preg_replace('/[^a-zA-Z]/', '', ucwords($resoprce));\n\n  \t$object = $app-\u003efactory($class, null, 'Interface'); // Interface\\UserCredit.php\n\n  \treturn [$object, $app-\u003emethod];\n    // convert path to file\n    // load file\n    // create class instance\n    // call method of that class\n\n    // TODO: think of some logical example here!!\n});\n```\n\n\n\n### Optional Arguments\n\nAgile API supports various get arguments.\n\n-   `?sort=name,-age` specify columns to sort by.\n-   `?q=search`, will attempt to perform full-text search by phrase. (if supported by persistence)\n-   `?condition[name]=value`, conditioning, but can also use `?name=value`\n-   `?limit=20`, return only 20 results at a time.\n-   `?skip=20`, skip first 20 results.\n-   `?only=name,surname` specify onlyFields\n-   `?ad={transformation}`, apply Agile Data transformation\n\nHandling of those arguments happens inside function `args()`. It's passed in a Model, so it will look at the GET arguments and perform the necessary changes.\n\n``` php\nfunction args(\\Atk4\\Data\\Model $m) {\n    if ($_GET['sort']) {\n        $m-\u003esortBy($_GET['sort']);\n    }\n\n    if ($_GET['condition']) {\n    \tforeach($_GET['condition'] as $key=\u003e$val) {\n            $m-\u003eaddCondition($key, $val);\n        }\n    }\n\n    if ($_GET['limit'] || $_GET['skip']) {\n        $m-\u003esetLimit($_GET['limit']?:null, $_GET['skip']?:null);\n    }\n\n    // etc. etc...\n}\n```\n\n### Other points\n\nAgile API is JSON only. You might be able to add XML output, but why.\n\nAgile API does not use envelope. Response data will be \"[]\" for empty result. If there is a problem with response, you'll get it through status code, in which case output will change.\n\nAgile API does not support HATEOAS. Technically you should be able to add support for it, but it would require a more complex mapping or extra code. We prefer to keep things simple.\n\nAgile API will pretty-print JSON by default, so make sure \"gzip\" is enabled.\n\nAgile API will accept either raw JSON or Form encoded input, but examples will always use JSON\n\nAgile API does not use \"pagination\" instead \"limit\" and \"skip\" values. You can introduce pages if you wish.\n\nDeep-loading resources is something that you can add. For instance if you load \"Invoice\" it may contain \"lines\" array containing list of hashes. Documentation will be provided on how to make this possible. There will also get argument to instruct if deep-loading is needed.\n\nErrors and exceptions will contain \"error\", \"message\" and \"args\" keys. Optional key \"raised_by\" may contain another object with same keys if said error was raised by another error. Another possibility is \"description\" field.\n\n(see http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api)\n\nhttps://www.reddit.com/r/PHP/comments/32tbxs/looking_for_php_rest_api_framework/\n\ntesting / behat: http://restler3.luracast.com/examples/_001_helloworld/readme.html\n\n### URL patterns\n\nHere are some examples\n\n-   `/user/:id`  matches /user/123 , /user/123/ , /user/abc/ but won't match /user/123/x\n-   `/user/:` same as above\n-   `/user/:/:` matches /user/123/321 but won't match /user/123\n-   `/user/*/:` matches /user/blah/123 but will ignore blah\n-   `/user/**/:` incorrect, as `**` must be last.\n-   `/user/:/**` matches /user/123/blah and /user/123/foo/blah and /user/123\n-   `/user/:id/:action?` optional parameter. If unspecified will be null\n\n### Route Groups\n\nIt's possible to divert route group to a different App.\n\n``` php\n$app = new \\Atk4\\Ui\\App\\Api();\n\n$app-\u003egroup('/user/**', function($app2) {\n   $app2-\u003eget('/test', function() {\n     return 'yes';\n   });\n});\n```\n\nYou can also divert\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatk4%2Fapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fatk4%2Fapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatk4%2Fapi/lists"}