liqueurdetoile / cakephp-apie
This Cakephp plugin allows to easily handle API calls within controllers and supports extended syntax to control data queries through url parameters
Installs: 3
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 0
Open Issues: 0
Type:cakephp-plugin
Requires
- php: ^7.2|^8.0
- cakephp/cakephp: ^4|^5
Requires (Dev)
- cakedc/cakephp-phpstan: ^2.0
- cakephp/cakephp-codesniffer: ^4.5
- cakephp/migrations: ^2.4|^3.2
- phpstan/extension-installer: ^1.1
- phpstan/phpstan: ^0.10|^1.8
- phpstan/phpstan-phpunit: ^1.2
- phpunit/phpunit: ^6|^8.5|^9.3
- vierge-noire/cakephp-fixture-factories: ^2.7
README
Cake as a Pie
This is a CakePHP plugin for 4.x/5.x branch.
The main idea of this plugin is to remotely leverage power of CakePHP query builder to configure advanced data queries through inbound URL parameter. Except for pagination, CakePHP framework is lacking tools to quicky achieve these goals. The plugin by itself is fairly agnostic about endpoints routing logic.
Quick overview
Let's say you want to fetch paginated articles written in 2020 for a given an author depending on its name and sort results by written date. Here's what you may do without the plugin in your endpoint :
// In your index method of your ArticlesController endpoint // URL may be /api/v1/articles?author_name=smith&from=2020-01-01&to=2020-12-31 in a REST context for instance $name = $this->request->getQuery('author_name'); $from = $this->request->getQuery('from'); $to = $this->request->getQuery('to'); $query = $this->Articles ->find() ->innerJoinWith('Authors', function($q) use ($name) { return $q->where(['Authors.name' => $name]); }) ->where(function ($exp) { return $exp->between('written', $from, $to); }) ->order(['written' => 'ASC']); $this->set('articles', $this->paginate($query));
Great, but to provide additional features, you'll have to manually handle routes and/or parameters per endpoint. Though it can be needed in some cases for security reasons, most of the time it will be great to be able to refine a query from client side. With this plugin, you can simply do this :
// In your index method of your ArticlesController endpoint // URL may be /api/v1/articles?q=%7B%22innerJoinWith%22%3A%7B%22Authors%22%3A%7B%22where%22%3A%7B%22Authors.name%22%3A%22Smith%22%7D%7D%7D%2C%22where%22%3A%7B%22between%22%3A%5B%22written%22%2C%222020-01-01%22%2C%222020-12-31%22%5D%7D%2C%22order%22%3A%7B%22written%22%3A%22ASC%22%7D%7D $query = $this->Api ->use($this->Articles) ->allow(['Authors']) // This is needed to allow access to Authors model from Articles endpoint side ->find(); $this->set('articles', $this->paginate($query));
The q
query parameter when url and json decoded turns into this PHP array which is the query descriptor :
[ 'innerJoinWith' => ['Authors', "()" => ['where' => [['Author.name' => 'Smith']]]], 'where' => ["()" => ['between' => ['written','2020-01-01','2020-12-31']]], 'order' => [['written' => 'ASC']], ]
It might be more comprehensive on JSON format :
{ "innerJoinWith": { "0": "Authors", "()": { "where": [{ "Author.name": "Smith" }] } }, "where": { "()": { "between": ["written", "2020-01-01", "2020-12-31"] } }, "order": [{ "written": "ASC" }] }
Relying on query builder ability to chain and provide callbacks, you can remotely configure a wide range of advanced queries this way. See below for more informations about the building of a usable query descriptor. See CakePHP query builder for more informations about available features.
Installation
Plugin is available through composer :
composer require liqueurdetoile/cakephp-apie
There's only a component in this plugin, therefore you're not required to explicitly add plugin at bootstrap step.
In your controller(s) that will use the component, simply load it during initialize
hook :
// In your controller public function initialize(): void { parent::initialize(); $this->loadComponent('Lqdt/CakephpApie.Api'); } // And use it through Api method public function index() { $articles = $this->Api ->use('Articles') ->find() ->all() // ... }
Component options
When initializing component, you can permanently alter the used query parameters name by submitting mapped default ones :
// In your controller $this->loadComponent('Lqdt/CakephpApie.Api', [ 'q' => 'whatever', // Permanently remap the query parameter monitored in URL to this value 'allowAll' => true, // Allow any associated data to be requested. Not recommended unless you know what you're doing ]);
With this config, component will now look into whatever
query parameter to find a query descriptor and will not check for allowance when associated data is requested.
Component methods
ApiComponent::use(\Cake\ORM\Table\|string $model) : self
Instruct component to use given table as base for the request. You can either provide a Table
instance or its name in TableRegistry
.
ApiComponent::setQueryParam(string $name) : self
Component will try to find a query descriptor from the $name
key in query string instead of the default q
key.
ApiComponent::allow(string|string[] $associations) : self
Configure component to allow the use of given associations by their names. Dotted paths are allowed.
TIP : Allowing a nested association automatically allows all intermediate associations in the path
ApiComponent::allowAll() : self
Disable association check for the current request.
TIP : Allowing any associated data to be fetched can be a very bad idea in most cases
ApiComponent::find() : \Cake\ORM\Query
Returns a query which have been initialized based on descriptor provided in url. It accepts the same parameters than any regular find
call.
ApiComponent::configure(\Cake\ORM\Query $query, array $descriptor) : \Cake\ORM\Query
Returns a query object which clauses have been initialized based on descriptor.
TIP You can use this feature if you're not planning to use url query parameter as descriptor source
Query descriptor syntax
A query descriptor is an array describing how to configure a query. Any available callable on a query can be used.
A very basic descriptor will look like :
[ "query_method1" => [/** arguments */] "query_method2" => [/** arguments */] ] // This will be used this way : call_user_func_array([$query, "query-method1"], [/** arguments */]; call_user_func_array([$query, "query-method2"], [/** arguments */];
Plugin expects that provided arguments are directly usable for call_user_func_array
, therefore they must be wrapped into an array.
If you need to call the same method multiple times, simply add +
as needed. They will be trimmed during parsing :
[ "query_method" => [/** arguments */] "+query_method" => [/** arguments */] "++query_method" => [/** arguments */] ] // This will be used this way : call_user_func_array([$query, "query-method"], [/** arguments */]; call_user_func_array([$query, "query-method"], [/** arguments */]; call_user_func_array([$query, "query-method"], [/** arguments */];
Query expressions
You can tell plugin that you're willing to use a query expression by using newExpr()
special key :
[ 'where' => [ 'newExpr()' => [ 'between' => ['date', '2020-01-01', '2020-12-31'] ] ] ]; // turns into call_user_func_array([$query, 'where'], [ call_user_func_array([$query->newExpr(), 'between'], ['date', '2020-01-01', '2020-12-31']) ]);
SQL functions
You can also use SQL functions with func()
special key :
[ 'select' => [ ['count' => [ 'func()' => [ 'count' => ['*'], ], ]], ], ]; // turns into call_user_func_array([$query, 'select'], [ call_user_func_array([$query->func(), 'count'], ['*']) ]);
Closures
Finally, you can tell plugin that you want to use a closure by using ()
special key. Except for associations closure, a query expression will be available and used to process nested descriptor.
[ 'where' => [ '()' => [ 'between' => ['date', '2020-01-01', '2020-12-31'] ] ] ]; // turns into call_user_func_array([$query, 'where', [ function (QueryExpression, $e) { call_user_func_array([$e, 'between'], ['date', '2020-01-01', '2020-12-31']); return $e; } ]]);
For associations, plugin will apply nested descriptor to the subquery in the closure :
[ 'contain' => [ 'Childs', '()' => [ 'where' => [['Childs.is_great' => true]] ] ] ]; // turns into call_user_func_array([$query, 'contain', [ 'Childs', function (Query $q) { call_user_func_array([$q, 'where'], [['Childs.is_great' => true]]); return $q; } ]]);