This project is no longer supported or maintained. If you need a modern version of Magic Box that is compatible with newer versions of Laravel please consider using the spiritual successor to this project — Koala Pouch.
Magic Box modularizes Fuzz's magical implementation of Laravel's Eloquent models as injectable, masked resource repositories.
- To create a two-way interchange format, so that the JSON representations of models broadcast by APIs can be re-applied back to their originating models for updating existing resources and creating new resources.
- Provide an interface for API clients to request exactly the data they want in the way they want.
-
composer require fuzz/magic-box
-
Use or extend
Fuzz\MagicBox\Middleware\RepositoryMiddleware
into your project and register your class under the$routeMiddleware
array inapp/Http/Kernel.php
.RepositoryMiddleware
contains a variety of configuration options that can be overridden -
If you're using
fuzz/api-server
, you can use magical routing by updatingapp/Providers/RouteServiceProvider.php
,RouteServiceProvider@map
, to include:/** * Define the routes for the application. * * @param \Illuminate\Routing\Router $router * @return void */ public function map(Router $router) { // Register a handy macro for registering resource routes $router->macro('restful', function ($model_name, $resource_controller = 'ResourceController') use ($router) { $alias = Str::lower(Str::snake(Str::plural(class_basename($model_name)), '-')); $router->resource($alias, $resource_controller, [ 'only' => [ 'index', 'store', 'show', 'update', 'destroy', ], ]); }); $router->group(['namespace' => $this->namespace], function ($router) { require app_path('Http/routes.php'); }); }
-
Set up your MagicBox resource routes under the middleware key you assign to your chosen
RepositoryMiddleware
class -
Set up a
YourAppNamespace\Http\Controllers\ResourceController
, here is what a ResourceController might look like . -
Set up models according to
Model Setup
section
Just run phpunit
after you composer install
.
Fuzz\MagicBox\EloquentRepository
implements a CRUD repository that cascades through relationships,
whether or not related models have been created yet.
Consider a simple model where a User has many Posts. EloquentRepository's basic usage is as follows:
Create a User with the username Steve who has a single Post with the title Stuff.
$repository = (new EloquentRepository)
->setModelClass('User')
->setInput([
'username' => 'steve',
'nonsense' => 'tomfoolery',
'posts' => [
'title' => 'Stuff',
],
]);
$user = $repository->save();
When $repository->save()
is invoked, a User will be created with the username "Steve", and a Post will
be created with the user_id
belonging to that User. The nonsensical "nonsense" property is simply
ignored, because it does not actually exist on the table storing Users.
By itself, EloquentRepository is a blunt weapon with no access controls that should be avoided in any public APIs. It will clobber every relationship it touches without prejudice. For example, the following is a BAD way to add a new Post for the user we just created.
$repository
->setInput([
'id' => $user->id,
'posts' => [
['title' => 'More Stuff'],
],
])
->save();
This will delete poor Steve's first post—not the intended effect. The safe(r) way to append a Post would be either of the following:
$repository
->setInput([
'id' => $user->id,
'posts' => [
['id' => $user->posts->first()->id],
['title' => 'More Stuff'],
],
])
->save();
$post = $repository
->setModelClass('Post')
->setInput([
'title' => 'More Stuff',
'user' => [
'id' => $user->id,
],
])
->save();
Generally speaking, the latter is preferred and is less likely to explode in your face.
The public API methods that return models from a repository are:
create
read
update
delete
save
, which will either callcreate
orupdate
depending on the state of its inputfind
, which will find a model by IDfindOrFail
, which will find a model by ID or throw\Illuminate\Database\Eloquent\ModelNotFoundException
The public API methods that return an \Illuminate\Database\Eloquent\Collection
are:
all
Fuzz\MagicBox\Filter
handles Eloquent Query Builder modifications based on filter values passed through the filters
parameter.
Tokens and usage:
Token | Description | Example |
---|---|---|
^ |
Field starts with | https://api.yourdomain.com/1.0/users?filters[name]=^John |
$ |
Field ends with | https://api.yourdomain.com/1.0/users?filters[name]=$Smith |
~ |
Field contains | https://api.yourdomain.com/1.0/users?filters[favorite_cheese]=~cheddar |
< |
Field is less than | https://api.yourdomain.com/1.0/users?filters[lifetime_value]=<50 |
> |
Field is greater than | https://api.yourdomain.com/1.0/users?filters[lifetime_value]=>50 |
>= |
Field is greater than or equals | https://api.yourdomain.com/1.0/users?filters[lifetime_value]=>=50 |
<= |
Field is less than or equals | https://api.yourdomain.com/1.0/users?filters[lifetime_value]=<=50 |
= |
Field is equal to | https://api.yourdomain.com/1.0/users?filters[username]==Specific%20Username |
!= |
Field is not equal to | https://api.yourdomain.com/1.0/users?filters[username]=!=common%20username |
[...] |
Field is one or more of | https://api.yourdomain.com/1.0/users?filters[id]=[1,5,10] |
![...] |
Field is not one of | https://api.yourdomain.com/1.0/users?filters[id]=![1,5,10] |
NULL |
Field is null | https://api.yourdomain.com/1.0/users?filters[address]=NULL |
NOT_NULL |
Field is not null | https://api.yourdomain.com/1.0/users?filters[email]=NOT_NULL |
Assuming we have users and their related tables resembling the following structure:
[
'username' => 'Bobby',
'profile' => [
'hobbies' => [
['name' => 'Hockey'],
['name' => 'Programming'],
['name' => 'Cooking']
]
]
]
We can filter by users' hobbies with users?filters[profile.hobbies.name]=^Cook
. Relationships can be of arbitrary
depth.
We can use AND
and OR
statements to build filters such as users?filters[username]==Bobby&filters[or][username]==Johnny&filters[and][profile.favorite_cheese]==Gouda
. The PHP array that's built from this filter is:
[
'username' => '=Bobby',
'or' => [
'username' => '=Johnny',
'and' => [
'profile.favorite_cheese' => '=Gouda',
]
]
]
and this filter can be read as select (users with username Bobby) OR (users with username Johnny who's profile.favorite_cheese attribute is Gouda)
.
Models need to implement Fuzz\MagicBox\Contracts\MagicBoxResource
before MagicBox will allow them to be exposed as a MagicBox resource. This is done so exposure is an explicit process and no more is exposed than is needed.
Models also need to define their own $fillable
array including attributes and relations that can be filled through this model. For example, if a User has many posts and has many comments but an API consumer should only be able to update comments through a user, the $fillable
array would look like:
protected $fillable = ['username', 'password', 'name', 'comments'];
MagicBox will only modify attributes/relations that are explicitly defined.
Magic Box is great and all, but we don't want to resolve model classes ourselves before we can instantiate a repository...
If you've configured a RESTful URI structure with pluralized resources (i.e. https://api.mydowmain.com/1.0/users
maps to the User model), you can use Fuzz\MagicBox\Utility\Modeler
to resolve a model class name from a route name.
phpunit
:)
- Route service provider should be pre-setup
- Support more relationships (esp. polymorphic relations) through cascading saves.
- Support paginating nested relations