├── src
├── .gitkeep
├── Contracts
│ ├── ModelResolver.php
│ ├── MagicBoxResource.php
│ ├── FilterInterface.php
│ └── Repository.php
├── Exception
│ └── ModelNotResolvedException.php
├── Providers
│ └── RepositoryServiceProvider.php
├── Utility
│ ├── RouteGuessingModelResolver.php
│ ├── ChecksRelations.php
│ └── ExplicitModelResolver.php
├── Middleware
│ └── RepositoryMiddleware.php
├── Filter.php
└── EloquentRepository.php
├── tests
├── .gitkeep
├── TestCase.php
├── DBTestCase.php
├── migrations
│ ├── 2015_01_01_000002_create_posts_table.php
│ ├── 2015_01_01_000001_create_users_table.php
│ ├── 2015_01_01_000003_create_profile_table.php
│ └── 2015_01_01_000004_create_tags_table.php
├── Models
│ ├── NotIncludable.php
│ ├── Profile.php
│ ├── Tag.php
│ ├── Post.php
│ └── User.php
├── ExplicitModelResolverTest.php
├── seeds
│ └── FilterDataSeeder.php
├── FilterTest.php
└── EloquentRepositoryTest.php
├── .travis.yml
├── .gitignore
├── config
└── magic-box.php
├── phpunit.xml
├── .codeclimate.yml
├── composer.json
├── LICENSE.md
├── phpmd.rulesets.xml
└── README.md
/src/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - '7.0'
4 | - '7.1'
5 | install:
6 | - composer install
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /components
3 | /coverage
4 | /tests/coverage
5 | composer.phar
6 | /tests/coverage
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/src/Contracts/ModelResolver.php:
--------------------------------------------------------------------------------
1 | 1
21 | ];
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
19 |
20 | src
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/DBTestCase.php:
--------------------------------------------------------------------------------
1 | artisan = $this->app->make('Illuminate\Contracts\Console\Kernel');
14 | $this->artisan->call(
15 | 'migrate', [
16 | '--database' => 'testbench',
17 | '--path' => '../../../../tests/migrations',
18 | ]
19 | );
20 | }
21 |
22 | protected function getEnvironmentSetUp($app)
23 | {
24 | parent::getEnvironmentSetUp($app);
25 |
26 | $app['config']->set('database.default', 'testbench');
27 | $app['config']->set(
28 | 'database.connections.testbench', [
29 | 'driver' => 'sqlite',
30 | 'database' => ':memory:',
31 | 'prefix' => ''
32 | ]
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | engines:
2 | duplication:
3 | enabled: true
4 | config:
5 | languages:
6 | - ruby
7 | - javascript
8 | - python
9 | - php
10 | fixme:
11 | enabled: true
12 | phpmd:
13 | enabled: true
14 | checks:
15 | Controversial/CamelCaseVariableName:
16 | enabled: false
17 | Controversial/CamelCasePropertyName:
18 | enabled: false
19 | Controversial/CamelCaseMethodName:
20 | enabled: false
21 | Controversial/CamelCaseParameterName:
22 | enabled: false
23 | config:
24 | file_extensions: "php"
25 | rulesets: "unusedcode,codesize,phpmd.rulesets.xml"
26 | ratings:
27 | paths:
28 | - "**.inc"
29 | - "**.js"
30 | - "**.jsx"
31 | - "**.module"
32 | - "**.php"
33 | - "**.py"
34 | - "**.rb"
35 | exclude_paths:
36 | - tests/
37 |
--------------------------------------------------------------------------------
/tests/migrations/2015_01_01_000002_create_posts_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
19 | $table->string('title');
20 | $table->string('not_fillable')->nullable();
21 | $table->string('not_filterable')->nullable();
22 | $table->unsignedInteger('user_id');
23 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
24 | $table->timestamps();
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('posts');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/migrations/2015_01_01_000001_create_users_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
19 | $table->string('username')->nullable();
20 | $table->string('name')->nullable();
21 | $table->integer('hands')->nullable();
22 | $table->integer('times_captured')->nullable();
23 | $table->string('occupation')->nullable();
24 | $table->string('not_fillable')->nullable();
25 | $table->string('not_filterable')->nullable();
26 | $table->softDeletes();
27 | $table->timestamps();
28 | }
29 | );
30 | }
31 |
32 | /**
33 | * Reverse the migrations.
34 | *
35 | * @return void
36 | */
37 | public function down()
38 | {
39 | Schema::dropIfExists('users');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fuzz/magic-box",
3 | "description": "A magical implementation of Laravel's Eloquent models as injectable, masked resource repositories.",
4 | "license": "MIT",
5 | "homepage": "https://fuzzproductions.com/",
6 | "require": {
7 | "php": "^7.0.0",
8 | "illuminate/database": "^5.0"
9 | },
10 | "require-dev": {
11 | "phpunit/phpunit": "~5.6",
12 | "orchestra/testbench": "3.3.*",
13 | "fuzz/http-exception": "1.0.*",
14 | "mockery/mockery": "0.9.*"
15 | },
16 | "authors": [
17 | {
18 | "name": "Fuzz Productions",
19 | "email": "fuzzweb@fuzzproductions.com"
20 | }
21 | ],
22 | "autoload": {
23 | "psr-4": {
24 | "Fuzz\\MagicBox\\": "src/"
25 | }
26 | },
27 | "autoload-dev": {
28 | "psr-4": {
29 | "Fuzz\\MagicBox\\Tests\\": "tests/",
30 | "Fuzz\\MagicBox\\Tests\\Seeds\\": "tests/seeds"
31 | }
32 | },
33 | "scripts": {
34 | "test": [
35 | "vendor/bin/phpunit"
36 | ],
37 | "test-coverage": [
38 | "vendor/bin/phpunit --coverage-html tests/coverage"
39 | ],
40 | "open-coverage": [
41 | "open -a \"Google Chrome\" tests/coverage/index.html"
42 | ]
43 | },
44 | "minimum-stability": "stable"
45 | }
46 |
--------------------------------------------------------------------------------
/tests/migrations/2015_01_01_000003_create_profile_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
19 | $table->unsignedInteger('user_id');
20 | $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
21 | $table->enum(
22 | 'favorite_cheese', [
23 | 'brie',
24 | 'pepper jack',
25 | 'Gouda',
26 | 'Cheddar',
27 | 'Provolone',
28 | ]);
29 | $table->string('favorite_fruit')->nullable();
30 | $table->boolean('is_human')->default(false);
31 | $table->string('not_fillable')->nullable();
32 | $table->string('not_filterable')->nullable();
33 | });
34 | }
35 |
36 | /**
37 | * Reverse the migrations.
38 | *
39 | * @return void
40 | */
41 | public function down()
42 | {
43 | Schema::dropIfExists('profiles');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2016 Fuzz Productions LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/src/Providers/RepositoryServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([$this->configPath() => config_path('magic-box.php')], 'config');
21 | }
22 |
23 | /**
24 | * Register any application services.
25 | *
26 | * @return void
27 | */
28 | public function register()
29 | {
30 | app()->singleton(Repository::class, function() {
31 | return new EloquentRepository;
32 | });
33 |
34 | app()->bind(ModelResolver::class, function() {
35 | return new ExplicitModelResolver;
36 | });
37 | }
38 |
39 | /**
40 | * Get the config path
41 | *
42 | * @return string
43 | */
44 | protected function configPath()
45 | {
46 | return realpath(__DIR__ . '/../../config/magic-box.php');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Contracts/FilterInterface.php:
--------------------------------------------------------------------------------
1 | increments('id');
19 | $table->string('label');
20 | $table->string('not_fillable')->nullable();
21 | });
22 |
23 | Schema::create(
24 | 'post_tag', function (Blueprint $table) {
25 | $table->increments('id');
26 | $table->unsignedInteger('post_id');
27 | $table->string('not_fillable')->nullable();
28 | $table->string('not_filterable')->nullable();
29 | $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
30 | $table->unsignedInteger('tag_id');
31 | $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
32 | $table->string('extra')->nullable();
33 | });
34 | }
35 |
36 | /**
37 | * Reverse the migrations.
38 | *
39 | * @return void
40 | */
41 | public function down()
42 | {
43 | Schema::dropIfExists('post_tag');
44 | Schema::dropIfExists('tags');
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Utility/RouteGuessingModelResolver.php:
--------------------------------------------------------------------------------
1 | getName();
25 |
26 | if (! is_null($route_name) && strpos($route_name, '.') !== false) {
27 | list(, $alias) = array_reverse(explode('.', $route->getName()));
28 |
29 | $model_class = $this->namespaceModel(Str::studly(Str::singular($alias)));
30 |
31 | if (is_a($model_class, MagicBoxResource::class, true)) {
32 | return $model_class;
33 | }
34 |
35 | throw new \LogicException(sprintf('%s must be an instance of %s', $model_class, MagicBoxResource::class));
36 | }
37 |
38 | throw new \LogicException('Unable to resolve model from improperly named route');
39 | }
40 |
41 | /**
42 | * Attach the app namespace to the model and return it.
43 | *
44 | * @param string $model_class
45 | * @return string
46 | */
47 | final public function namespaceModel($model_class)
48 | {
49 | return sprintf('%s%s', $this->getAppNamespace(), $model_class);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Models/Profile.php:
--------------------------------------------------------------------------------
1 | belongsTo(User::class);
52 | }
53 |
54 | /**
55 | * Get the list of fields fillable by the repository
56 | *
57 | * @return array
58 | */
59 | public function getRepositoryFillable(): array
60 | {
61 | return self::FILLABLE;
62 | }
63 |
64 | /**
65 | * Get the list of relationships fillable by the repository
66 | *
67 | * @return array
68 | */
69 | public function getRepositoryIncludable(): array
70 | {
71 | return self::INCLUDABLE;
72 | }
73 |
74 | /**
75 | * Get the list of fields filterable by the repository
76 | *
77 | * @return array
78 | */
79 | public function getRepositoryFilterable(): array
80 | {
81 | return self::FILTERABLE;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/phpmd.rulesets.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | Custom rules for checking my project
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 | 3
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/tests/Models/Tag.php:
--------------------------------------------------------------------------------
1 | belongsToMany(Post::class)->withPivot('extra');
59 | }
60 |
61 | /**
62 | * Get the list of fields fillable by the repository
63 | *
64 | * @return array
65 | */
66 | public function getRepositoryFillable(): array
67 | {
68 | return self::FILLABLE;
69 | }
70 |
71 | /**
72 | * Get the list of relationships fillable by the repository
73 | *
74 | * @return array
75 | */
76 | public function getRepositoryIncludable(): array
77 | {
78 | return self::INCLUDABLE;
79 | }
80 |
81 | /**
82 | * Get the list of fields filterable by the repository
83 | *
84 | * @return array
85 | */
86 | public function getRepositoryFilterable(): array
87 | {
88 | return self::FILTERABLE;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tests/Models/Post.php:
--------------------------------------------------------------------------------
1 | belongsTo(NotIncludable::class);
51 | }
52 |
53 | /**
54 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
55 | */
56 | public function user()
57 | {
58 | return $this->belongsTo(User::class);
59 | }
60 |
61 | /**
62 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
63 | */
64 | public function tags()
65 | {
66 | return $this->belongsToMany(Tag::class)->withPivot('extra');
67 | }
68 |
69 | /**
70 | * Get the list of fields fillable by the repository
71 | *
72 | * @return array
73 | */
74 | public function getRepositoryFillable(): array
75 | {
76 | return self::FILLABLE;
77 | }
78 |
79 | /**
80 | * Get the list of relationships fillable by the repository
81 | *
82 | * @return array
83 | */
84 | public function getRepositoryIncludable(): array
85 | {
86 | return self::INCLUDABLE;
87 | }
88 |
89 | /**
90 | * Get the list of fields filterable by the repository
91 | *
92 | * @return array
93 | */
94 | public function getRepositoryFilterable(): array
95 | {
96 | return self::FILTERABLE;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Utility/ChecksRelations.php:
--------------------------------------------------------------------------------
1 | __toString();
36 |
37 | $supported_relations = [
38 | BelongsTo::class,
39 | HasOne::class,
40 | HasMany::class,
41 | BelongsToMany::class,
42 | ];
43 |
44 | // Find which, if any, of the supported relations are present in the reflected string
45 | foreach ($supported_relations as $supported_relation) {
46 | if (strpos($reflected_method, $supported_relation) !== false) {
47 | $relation = $instance->$key();
48 | break;
49 | }
50 | }
51 |
52 | // If the ReflectionMethod guess fails, try to guess based on the concrete return type of a safe instance
53 | // of the model
54 | if (is_null($relation) && ($safe_instance->$key() instanceof Relation)) {
55 | // If the method returns a Relation, we can safely call it
56 | $relation = $instance->$key();
57 | }
58 |
59 | return is_null($relation) ? false : $relation;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Middleware/RepositoryMiddleware.php:
--------------------------------------------------------------------------------
1 | buildRepository($request);
22 |
23 | return $next($request);
24 | }
25 |
26 | /**
27 | * Build a repository based on inbound request data.
28 | *
29 | * @param \Illuminate\Http\Request $request
30 | * @return \Fuzz\MagicBox\EloquentRepository
31 | */
32 | public function buildRepository(Request $request): EloquentRepository
33 | {
34 | $input = [];
35 |
36 | /** @var \Illuminate\Routing\Route $route */
37 | $route = $request->route();
38 |
39 | // Resolve the model class if possible. And setup the repository.
40 | /** @var \Illuminate\Database\Eloquent\Model $model_class */
41 | $model_class = resolve(ModelResolver::class)->resolveModelClass($route);
42 |
43 | // Look for /{model-class}/{id} RESTful requests
44 | $parameters = $route->parametersWithoutNulls();
45 | if (! empty($parameters)) {
46 | $id = reset($parameters);
47 | $input = compact('id');
48 | }
49 |
50 | // If the method is not GET lets get the input from everywhere.
51 | // @TODO hmm, need to verify what happens on DELETE and PATCH.
52 | if ($request->method() !== 'GET') {
53 | $input += $request->all();
54 | }
55 |
56 | // Resolve an eloquent repository bound to our standardized route parameter
57 | $repository = resolve(Repository::class);
58 |
59 | $repository->setModelClass($model_class)
60 | ->setFilters((array) $request->get('filters'))
61 | ->setSortOrder((array) $request->get('sort'))
62 | ->setGroupBy((array) $request->get('group'))
63 | ->setEagerLoads((array) $request->get('include'))
64 | ->setAggregate((array) $request->get('aggregate'))
65 | ->setDepthRestriction(config('magic-box.eager_load_depth'))
66 | ->setInput($input);
67 | return $repository;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/Models/User.php:
--------------------------------------------------------------------------------
1 | hasMany(Post::class);
61 | }
62 |
63 | /**
64 | * @return \Illuminate\Database\Eloquent\Relations\HasOne
65 | */
66 | public function profile()
67 | {
68 | return $this->hasOne(Profile::class);
69 | }
70 |
71 | /**
72 | * For unit testing purposes
73 | *
74 | * @return array
75 | */
76 | public function getFillable()
77 | {
78 | return $this->fillable;
79 | }
80 |
81 | /**
82 | * For unit testing purposes
83 | *
84 | * @param array $fillable
85 | *
86 | * @return $this
87 | */
88 | public function setFillable(array $fillable)
89 | {
90 | $this->fillable = $fillable;
91 |
92 | return $this;
93 | }
94 |
95 | /**
96 | * Get the list of fields fillable by the repository
97 | *
98 | * @return array
99 | */
100 | public function getRepositoryFillable(): array
101 | {
102 | return self::FILLABLE;
103 | }
104 |
105 | /**
106 | * Get the list of relationships fillable by the repository
107 | *
108 | * @return array
109 | */
110 | public function getRepositoryIncludable(): array
111 | {
112 | return self::INCLUDABLE;
113 | }
114 |
115 | /**
116 | * Get the list of fields filterable by the repository
117 | *
118 | * @return array
119 | */
120 | public function getRepositoryFilterable(): array
121 | {
122 | return self::FILTERABLE;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/tests/ExplicitModelResolverTest.php:
--------------------------------------------------------------------------------
1 | get('/users', ['resource' => User::class, 'uses' => 'UserController@getUsers']);
25 | */
26 | public function testCanResolveModelClassOnRoute()
27 | {
28 | $route = Mockery::mock(Route::class);
29 | $route->shouldReceive('getAction')->twice()->andReturn([
30 | 'resource' => User::class
31 | ]);
32 |
33 | $model = (new ExplicitModelResolver())->resolveModelClass($route);
34 |
35 | $this->assertEquals(User::class, $model);
36 | $this->assertNotEquals(Post::class, $model);
37 | }
38 |
39 | /**
40 | * Given a controller has a static property named $resource
41 | * When the model is resolved
42 | * Then the resolver should return the properties value.
43 | *
44 | * Example:
45 | * $router->get('/users', ['uses' => 'UserController@getUsers']);
46 | *
47 | * class UserController extends BaseController {
48 | * ...
49 | *
50 | * public static $resource = User::class;
51 | *
52 | * ...
53 | * }
54 | */
55 | public function testCanResolveModelClassOnController()
56 | {
57 | $route = Mockery::mock(Route::class);
58 | $route->shouldReceive('getAction')->zeroOrMoreTimes()->andReturn([
59 | 'uses' => 'Fuzz\MagicBox\Tests\UserController@getUsers'
60 | ]);
61 |
62 | $model = (new ExplicitModelResolver())->resolveModelClass($route);
63 |
64 | $this->assertEquals(User::class, $model);
65 | $this->assertNotEquals(Post::class, $model);
66 | }
67 |
68 | /**
69 | * Given the route is a closure
70 | * When the model is resolved
71 | * Then the resolver should return false because it cannot be resolved.
72 | *
73 | * Example:
74 | * $router->get('/users', function($request) {
75 | * return response()->json([], 200);
76 | * })
77 | */
78 | public function testItThrowsModelNotResolvedExceptionIfModelCouldNotBeResolved()
79 | {
80 | $route = Mockery::mock(Route::class);
81 | $route->shouldReceive('getAction')->zeroOrMoreTimes()->andReturn([
82 | 'uses' => function() {
83 | return null;
84 | }
85 | ]);
86 |
87 | $this->expectException(ModelNotResolvedException::class);
88 | $model = (new ExplicitModelResolver())->resolveModelClass($route);
89 | }
90 | }
91 |
92 |
93 | /**
94 | * Class UserController
95 | *
96 | * @package Fuzz\MagicBox\Tests
97 | */
98 | class UserController extends Controller
99 | {
100 | public static $resource = User::class;
101 | }
--------------------------------------------------------------------------------
/src/Utility/ExplicitModelResolver.php:
--------------------------------------------------------------------------------
1 | Route Group -> Route.
25 | */
26 | public function resolveModelClass(Route $route): string
27 | {
28 | // If the route has a resource property we can instantly resolve the model.
29 | if ($this->routeHasResource($route)) {
30 | return $this->getRouteResource($route);
31 | }
32 |
33 | // If the action is a Closure instance, and the route does not have
34 | // a resource property then we can not resolve to a model.
35 | if ($this->actionIsCallable($route->getAction())) {
36 | // Model could not be resolved
37 | throw new ModelNotResolvedException;
38 | }
39 |
40 | // If the routes controller has a resource set then return that resource.
41 | if ($this->controllerHasResource($controller = $this->getRouteController($route))) {
42 | return $this->getControllerResource($controller);
43 | }
44 |
45 | // Model could not be resolved
46 | throw new ModelNotResolvedException;
47 | }
48 | /**
49 | * Checks the route for a resource property.
50 | *
51 | * @param \Illuminate\Routing\Route $route
52 | *
53 | * @return bool
54 | */
55 | public function routeHasResource(Route $route)
56 | {
57 | return array_key_exists('resource', $route->getAction());
58 | }
59 | /**
60 | * Get the resource property from a route.
61 | *
62 | * @param \Illuminate\Routing\Route $route
63 | *
64 | * @return string
65 | */
66 | public function getRouteResource(Route $route)
67 | {
68 | return $route->getAction()['resource'];
69 | }
70 | /**
71 | * Checks if the action uses a callable.
72 | *
73 | * @param $action
74 | *
75 | * @return bool
76 | */
77 | public function actionIsCallable($action)
78 | {
79 | return (is_callable($action['uses']));
80 | }
81 | /**
82 | * Check if the controller has a resource.
83 | *
84 | * @param $controller
85 | *
86 | * @return bool
87 | */
88 | public function controllerHasResource($controller)
89 | {
90 | return isset($controller::$resource);
91 | }
92 | /**
93 | * Get the routes controller.
94 | *
95 | * @param \Illuminate\Routing\Route $route
96 | *
97 | * @return string
98 | */
99 | public function getRouteController(Route $route)
100 | {
101 | return explode('@', $route->getAction()['uses'])[0];
102 | }
103 | /**
104 | * Get the controllers resource.
105 | *
106 | * @param $controller
107 | *
108 | * @return mixed
109 | */
110 | public function getControllerResource($controller)
111 | {
112 | return $controller::$resource;
113 | }
114 | /**
115 | * Get the routes method.
116 | *
117 | * @param \Illuminate\Routing\Route $route
118 | *
119 | * @return mixed
120 | */
121 | public function getRouteMethod(Route $route)
122 | {
123 | return explode('@', $route->getAction()['uses'])[1];
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tests/seeds/FilterDataSeeder.php:
--------------------------------------------------------------------------------
1 | users() as $user) {
20 | $user_instance = new User;
21 |
22 | foreach (
23 | [
24 | 'username',
25 | 'name',
26 | 'hands',
27 | 'times_captured',
28 | 'occupation',
29 | ] as $attribute
30 | ) {
31 | $user_instance->{$attribute} = $user[$attribute];
32 | }
33 |
34 | $user_instance->save();
35 |
36 | $profile = new Profile;
37 | foreach ($user['profile'] as $key => $value) {
38 | $profile->{$key} = $value;
39 | }
40 | $profile->user_id = $user_instance->id;
41 | $profile->save();
42 |
43 | foreach ($user['posts'] as $post) {
44 | $post_instance = new Post;
45 | $post_instance->title = $post['title'];
46 | $post_instance->user_id = $user_instance->id;
47 | $post_instance->save();
48 |
49 | $tag_ids = [];
50 | foreach ($post['tags'] as $tag) {
51 | $tag_instance = new Tag;
52 | $tag_instance->label = $tag['label'];
53 | $tag_instance->save();
54 | $tag_ids[] = $tag_instance->id;
55 | }
56 |
57 | $post_instance->tags()->sync($tag_ids);
58 | }
59 | }
60 |
61 | $users = User::with(
62 | [
63 | 'profile',
64 | 'posts.tags'
65 | ]
66 | )->get()->toArray();
67 | $test = 'test';
68 | }
69 |
70 | public function users()
71 | {
72 | return [
73 | [
74 | 'username' => 'lskywalker@galaxyfarfaraway.com',
75 | 'name' => 'Luke Skywalker',
76 | 'hands' => 1,
77 | 'times_captured' => 4,
78 | 'occupation' => 'Jedi',
79 | 'profile' => [
80 | 'favorite_cheese' => 'Gouda',
81 | 'favorite_fruit' => 'Apples',
82 | 'is_human' => true
83 | ],
84 | 'posts' => [
85 | [
86 | 'title' => 'I Kissed a Princess and I Liked it',
87 | 'tags' => [
88 | ['label' => '#peace',],
89 | ['label' => '#thelastjedi',]
90 | ]
91 | ]
92 | ]
93 | ],
94 | [
95 | 'username' => 'lorgana@galaxyfarfaraway.com',
96 | 'name' => 'Leia Organa',
97 | 'hands' => 2,
98 | 'times_captured' => 6,
99 | 'occupation' => null,
100 | 'profile' => [
101 | 'favorite_cheese' => 'Provolone',
102 | 'favorite_fruit' => 'Mystery Berries',
103 | 'is_human' => true
104 | ],
105 | 'posts' => [
106 | [
107 | 'title' => 'Smugglers: A Girl\'s Dream',
108 | 'tags' => [
109 | ['label' => '#princess',],
110 | ['label' => '#mysonistheworst',],
111 | ]
112 | ]
113 | ]
114 | ],
115 | [
116 | 'username' => 'solocup@galaxyfarfaraway.com',
117 | 'name' => 'Han Solo',
118 | 'hands' => 2,
119 | 'times_captured' => 1,
120 | 'occupation' => 'Smuggler',
121 | 'profile' => [
122 | 'favorite_cheese' => 'Cheddar',
123 | 'favorite_fruit' => null,
124 | 'is_human' => true
125 | ],
126 | 'posts' => [
127 | [
128 | 'title' => '10 Easy Ways to Clean Fur From Couches',
129 | 'tags' => [
130 | ['label' => '#iknow',],
131 | ['label' => '#triggerfinger',],
132 | ['label' => '#mysonistheworst',],
133 | ]
134 | ]
135 | ]
136 | ],
137 | [
138 | 'username' => 'chewbaclava@galaxyfarfaraway.com',
139 | 'name' => 'Chewbacca',
140 | 'hands' => 0,
141 | 'times_captured' => 0,
142 | 'occupation' => 'Smuggler\'s Assistant',
143 | 'profile' => [
144 | 'favorite_cheese' => 'brie',
145 | 'favorite_fruit' => null,
146 | 'is_human' => false
147 | ],
148 | 'posts' => [
149 | [
150 | 'title' => 'Rrrrrrr-ghghg Rrrr-ghghghghgh Rrrr-ghghghgh!',
151 | 'tags' => [
152 | ['label' => '#starwarsfurlife',],
153 | ['label' => '#chewonthis',],
154 | ]
155 | ]
156 | ]
157 | ],
158 | ];
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/Contracts/Repository.php:
--------------------------------------------------------------------------------
1 | macro('restful', function ($model_name, $resource_controller = 'ResourceController') use ($router) {
31 | $alias = Str::lower(Str::snake(Str::plural(class_basename($model_name)), '-'));
32 |
33 | $router->resource($alias, $resource_controller, [
34 | 'only' => [
35 | 'index',
36 | 'store',
37 | 'show',
38 | 'update',
39 | 'destroy',
40 | ],
41 | ]);
42 | });
43 |
44 | $router->group(['namespace' => $this->namespace], function ($router) {
45 | require app_path('Http/routes.php');
46 | });
47 | }
48 | ```
49 | 1. Set up your MagicBox resource routes under the middleware key you assign to your chosen `RepositoryMiddleware` class
50 | 1. Set up a `YourAppNamespace\Http\Controllers\ResourceController`, [here is what a ResourceController might look like](https://gist.github.com/SimantovYousoufov/dea19adb1dfd8f05c1fcad9db976c247) .
51 | 1. Set up models according to `Model Setup` section
52 |
53 | ## Testing
54 | Just run `phpunit` after you `composer install`.
55 |
56 | ## Eloquent Repository
57 | `Fuzz\MagicBox\EloquentRepository` implements a CRUD repository that cascades through relationships,
58 | whether or not related models have been created yet.
59 |
60 | Consider a simple model where a User has many Posts. EloquentRepository's basic usage is as follows:
61 |
62 | Create a User with the username Steve who has a single Post with the title Stuff.
63 |
64 | ```php
65 | $repository = (new EloquentRepository)
66 | ->setModelClass('User')
67 | ->setInput([
68 | 'username' => 'steve',
69 | 'nonsense' => 'tomfoolery',
70 | 'posts' => [
71 | 'title' => 'Stuff',
72 | ],
73 | ]);
74 |
75 | $user = $repository->save();
76 | ```
77 |
78 | When `$repository->save()` is invoked, a User will be created with the username "Steve", and a Post will
79 | be created with the `user_id` belonging to that User. The nonsensical "nonsense" property is simply
80 | ignored, because it does not actually exist on the table storing Users.
81 |
82 | By itself, EloquentRepository is a blunt weapon with no access controls that should be avoided in any
83 | public APIs. It will clobber every relationship it touches without prejudice. For example, the following
84 | is a BAD way to add a new Post for the user we just created.
85 |
86 | ```php
87 | $repository
88 | ->setInput([
89 | 'id' => $user->id,
90 | 'posts' => [
91 | ['title' => 'More Stuff'],
92 | ],
93 | ])
94 | ->save();
95 | ```
96 |
97 | This will delete poor Steve's first post—not the intended effect. The safe(r) way to append a Post
98 | would be either of the following:
99 |
100 | ```php
101 | $repository
102 | ->setInput([
103 | 'id' => $user->id,
104 | 'posts' => [
105 | ['id' => $user->posts->first()->id],
106 | ['title' => 'More Stuff'],
107 | ],
108 | ])
109 | ->save();
110 | ```
111 |
112 | ```php
113 | $post = $repository
114 | ->setModelClass('Post')
115 | ->setInput([
116 | 'title' => 'More Stuff',
117 | 'user' => [
118 | 'id' => $user->id,
119 | ],
120 | ])
121 | ->save();
122 | ```
123 |
124 | Generally speaking, the latter is preferred and is less likely to explode in your face.
125 |
126 | The public API methods that return models from a repository are:
127 |
128 | 1. `create`
129 | 1. `read`
130 | 1. `update`
131 | 1. `delete`
132 | 1. `save`, which will either call `create` or `update` depending on the state of its input
133 | 1. `find`, which will find a model by ID
134 | 1. `findOrFail`, which will find a model by ID or throw `\Illuminate\Database\Eloquent\ModelNotFoundException`
135 |
136 | The public API methods that return an `\Illuminate\Database\Eloquent\Collection` are:
137 |
138 | 1. `all`
139 |
140 | ## Filtering
141 | `Fuzz\MagicBox\Filter` handles Eloquent Query Builder modifications based on filter values passed through the `filters`
142 | parameter.
143 |
144 | Tokens and usage:
145 |
146 | | Token | Description | Example |
147 | |:----------:|:-------------------------------:|:----------------------------------------------:|
148 | | `^` | Field starts with | `https://api.yourdomain.com/1.0/users?filters[name]=^John` |
149 | | `$` | Field ends with | `https://api.yourdomain.com/1.0/users?filters[name]=$Smith` |
150 | | `~` | Field contains | `https://api.yourdomain.com/1.0/users?filters[favorite_cheese]=~cheddar` |
151 | | `<` | Field is less than | `https://api.yourdomain.com/1.0/users?filters[lifetime_value]=<50` |
152 | | `>` | Field is greater than | `https://api.yourdomain.com/1.0/users?filters[lifetime_value]=>50` |
153 | | `>=` | Field is greater than or equals | `https://api.yourdomain.com/1.0/users?filters[lifetime_value]=>=50` |
154 | | `<=` | Field is less than or equals | `https://api.yourdomain.com/1.0/users?filters[lifetime_value]=<=50` |
155 | | `=` | Field is equal to | `https://api.yourdomain.com/1.0/users?filters[username]==Specific%20Username` |
156 | | `!=` | Field is not equal to | `https://api.yourdomain.com/1.0/users?filters[username]=!=common%20username` |
157 | | `[...]` | Field is one or more of | `https://api.yourdomain.com/1.0/users?filters[id]=[1,5,10]` |
158 | | `![...]` | Field is not one of | `https://api.yourdomain.com/1.0/users?filters[id]=![1,5,10]` |
159 | | `NULL` | Field is null | `https://api.yourdomain.com/1.0/users?filters[address]=NULL` |
160 | | `NOT_NULL` | Field is not null | `https://api.yourdomain.com/1.0/users?filters[email]=NOT_NULL` |
161 |
162 | ### Filtering relations
163 | Assuming we have users and their related tables resembling the following structure:
164 |
165 | ```php
166 | [
167 | 'username' => 'Bobby',
168 | 'profile' => [
169 | 'hobbies' => [
170 | ['name' => 'Hockey'],
171 | ['name' => 'Programming'],
172 | ['name' => 'Cooking']
173 | ]
174 | ]
175 | ]
176 | ```
177 |
178 | We can filter by users' hobbies with `users?filters[profile.hobbies.name]=^Cook`. Relationships can be of arbitrary
179 | depth.
180 |
181 | ### Filter conjuctions
182 | 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:
183 |
184 | ```php
185 | [
186 | 'username' => '=Bobby',
187 | 'or' => [
188 | 'username' => '=Johnny',
189 | 'and' => [
190 | 'profile.favorite_cheese' => '=Gouda',
191 | ]
192 | ]
193 | ]
194 | ```
195 |
196 | 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)`.
197 |
198 | ## Model Setup
199 | 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.
200 |
201 | 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:
202 |
203 | ```
204 | protected $fillable = ['username', 'password', 'name', 'comments'];
205 | ```
206 |
207 | MagicBox will only modify attributes/relations that are explicitly defined.
208 |
209 | ## Resolving models
210 | Magic Box is great and all, but we don't want to resolve model classes ourselves before we can instantiate a repository...
211 |
212 | 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.
213 |
214 | ## Testing
215 | `phpunit` :)
216 |
217 | ### TODO
218 | 1. Route service provider should be pre-setup
219 | 1. Support more relationships (esp. polymorphic relations) through cascading saves.
220 | 1. Support paginating nested relations
221 |
--------------------------------------------------------------------------------
/src/Filter.php:
--------------------------------------------------------------------------------
1 | 'startsWith',
16 | '~' => 'contains',
17 | '$' => 'endsWith',
18 | '<' => 'lessThan',
19 | '>' => 'greaterThan',
20 | '>=' => 'greaterThanOrEquals',
21 | '<=' => 'lessThanOrEquals',
22 | '=' => 'equals',
23 | '!=' => 'notEquals',
24 | '![' => 'notIn',
25 | '[' => 'in',
26 | ];
27 |
28 | /**
29 | * Tokens that accept non-scalar filters.
30 | * ex: [One,Two,Three,Fuzz]
31 | *
32 | * @var array
33 | */
34 | protected static $non_scalar_tokens = [
35 | '![',
36 | '[',
37 | ];
38 |
39 | /**
40 | * Container for base table prefix. Always specify table.
41 | *
42 | * @var null
43 | */
44 | protected static $table_prefix = null;
45 |
46 | /**
47 | * Clean a set of filters by checking them against an array of allowed filters
48 | *
49 | * This is similar to an array intersect, if a $filter is present in $allowed and set to true,
50 | * then it is an allowed filter.
51 | *
52 | * $filters = [
53 | * 'foo' => 'bar',
54 | * 'and' => [
55 | * 'baz' => 'bat'
56 | * 'or' => [
57 | * 'bag' => 'boo'
58 | * ]
59 | * ],
60 | * 'or' => [
61 | * 'bar' => 'foo'
62 | * ],
63 | * ];
64 | *
65 | * $allowed = [
66 | * 'foo' => true,
67 | * 'baz' => true,
68 | * 'baz' => true,
69 | * 'bar' => true,
70 | * ];
71 | *
72 | * $result = [
73 | * 'foo' => 'bar',
74 | * 'and' => [
75 | * 'baz' => 'bat'
76 | * ],
77 | * 'or' => [
78 | * 'bar' => 'foo'
79 | * ],
80 | * ];
81 | *
82 | * @param array $filters
83 | * @param array $allowed
84 | *
85 | * @return array
86 | */
87 | public static function intersectAllowedFilters(array $filters, array $allowed)
88 | {
89 | foreach ($filters as $filter => $value) {
90 | // We want to recursively go down and check all OR conjuctions to ensure they're all whitlisted
91 | if ($filter === 'or') {
92 | $filters['or'] = self::intersectAllowedFilters($filters['or'], $allowed);
93 |
94 | // If there are no more filters under this OR, we can safely unset it
95 | if (count($filters['or']) === 0) {
96 | unset($filters['or']);
97 | }
98 | continue;
99 | }
100 |
101 | // We want to recursively go down and check all AND conjuctions to ensure they're all whitlisted
102 | if ($filter === 'and') {
103 | $filters['and'] = self::intersectAllowedFilters($filters['and'], $allowed);
104 |
105 | // If there are no more filters under this AND, we can safely unset it
106 | if (count($filters['and']) === 0) {
107 | unset($filters['and']);
108 | }
109 | continue;
110 | }
111 |
112 | // A whitelisted filter looks like 'filter_name' => true in $allowed
113 | if (! isset($allowed[$filter]) || ! $allowed[$filter]) {
114 | unset($filters[$filter]);
115 | }
116 | }
117 |
118 | return $filters;
119 | }
120 |
121 | /**
122 | * Funnel for rest of filter methods
123 | *
124 | * @param \Illuminate\Database\Eloquent\Builder $query
125 | * @param array $filters
126 | * @param array $columns
127 | * @param string $table
128 | */
129 | public static function applyQueryFilters($query, $filters, $columns, $table)
130 | {
131 | // Wrap in a complex where so we don't break soft delete checks
132 | $query->where(
133 | function ($query) use ($filters, $columns, $table) {
134 | self::filterQuery($query, $filters, $columns, $table);
135 | });
136 | }
137 |
138 | /**
139 | * Funnel method to filter queries.
140 | *
141 | * First check for a dot nested string in the place of a filter column and use the appropriate method
142 | * and relation combination.
143 | *
144 | * @param \Illuminate\Database\Eloquent\Builder $query
145 | * @param array $filters
146 | * @param array $columns
147 | * @param string $table
148 | */
149 | public static function filterQuery($query, $filters, $columns, $table)
150 | {
151 | if (! is_null($table)) {
152 | self::$table_prefix = $table;
153 | }
154 |
155 | foreach ($filters as $column => $filter) {
156 | if (strtolower($column) === 'or' || strtolower($column) === 'and') {
157 | $nextConjunction = $column === 'or';
158 | $method = self::determineMethod('where', $nextConjunction);
159 |
160 | // orWhere should only occur on conjunctions. We want filters in the same nesting level to attach as
161 | // 'AND'. 'OR' should nest.
162 | $query->$method(
163 | function ($query) use ($filters, $columns, $column, $table) {
164 | self::filterQuery($query, $filters[$column], $columns, $table);
165 | });
166 | continue;
167 | }
168 |
169 | $nested_relations = self::parseRelations($column);
170 |
171 | if (is_array($nested_relations)) {
172 | // Create a dot nested string of relations
173 | $relation = implode('.', array_splice($nested_relations, 0, count($nested_relations) - 1));
174 | // Set up the column at the end of the dot nested relation
175 | $column = end($nested_relations);
176 | }
177 |
178 | if ($token = self::determineTokenType($filter)) {
179 | // We check to see if the filter string is a valid filter.
180 | $filter = self::cleanAndValidateFilter($token, $filter);
181 |
182 | // If it is not a valid filter we continue to the next
183 | // iteration in the array.
184 | if ($filter === false) {
185 | continue;
186 | }
187 |
188 | $method = self::$supported_tokens[$token];
189 |
190 | // Querying a dot nested relation
191 | if (is_array($nested_relations)) {
192 |
193 | $query->whereHas(
194 | $relation, function ($query) use ($method, $column, $filter) {
195 |
196 | // Check if the column is a primary key of the model
197 | // within the query. If it is, we should use the
198 | // qualified key instead. It's important when this is a
199 | // many to many relationship query.
200 | if ($column === $query->getModel()->getKeyName()) {
201 | $column = $query->getModel()->getQualifiedKeyName();
202 | }
203 |
204 | self::$method($column, $filter, $query);
205 | });
206 | } else {
207 | $column = self::applyTablePrefix($column);
208 | self::$method($column, $filter, $query);
209 | }
210 | } elseif ($filter === 'true' || $filter === 'false') {
211 | // Is a boolean filter, coerce to boolean.
212 | $filter = ($filter === 'true');
213 |
214 | // Querying a dot nested relation
215 | if (is_array($nested_relations)) {
216 | $query->whereHas(
217 | $relation, function ($query) use ($filter, $column) {
218 | $where = camel_case('where' . $column);
219 | $query->$where($filter);
220 | });
221 | } else {
222 | $column = self::applyTablePrefix($column);
223 | $where = camel_case('where' . $column);
224 | $query->$where($filter);
225 | }
226 | } elseif ($filter === 'NULL' || $filter === 'NOT_NULL') {
227 | // Querying a dot nested relation
228 | if (is_array($nested_relations)) {
229 | $query->whereHas(
230 | $relation, function ($query) use ($column, $filter) {
231 | self::nullMethod($column, $filter, $query);
232 | });
233 | } else {
234 | $column = self::applyTablePrefix($column);
235 | self::nullMethod($column, $filter, $query);
236 | }
237 | } else {
238 | // @todo Unsupported type
239 | }
240 | }
241 | }
242 |
243 | /**
244 | * Parse a string of dot nested relations, if applicable
245 | *
246 | * Ex: users?filters[posts.comments.rating]=>4
247 | *
248 | * @param string $filter_name
249 | *
250 | * @return array
251 | */
252 | protected static function parseRelations($filter_name)
253 | {
254 | // Determine if we're querying a dot nested relationships of arbitrary depth (ex: user.post.tags.label)
255 | $parse_relations = explode('.', $filter_name);
256 |
257 | return count($parse_relations) === 1 ? $parse_relations[0] : $parse_relations;
258 | }
259 |
260 | /**
261 | * Query for items that begin with a string.
262 | *
263 | * Ex: users?filters[name]=^John
264 | *
265 | * @param string $column
266 | * @param string $filter
267 | * @param \Illuminate\Database\Eloquent\Builder $query
268 | * @param bool $or
269 | */
270 | protected static function startsWith($column, $filter, $query, $or = false)
271 | {
272 | $method = self::determineMethod('where', $or);
273 | $query->$method($column, 'LIKE', $filter . '%');
274 | }
275 |
276 | /**
277 | * Query for items that end with a string.
278 | *
279 | * Ex: users?filters[name]=$Smith
280 | *
281 | * @param string $column
282 | * @param string $filter
283 | * @param \Illuminate\Database\Eloquent\Builder $query
284 | * @param bool $or
285 | */
286 | protected static function endsWith($column, $filter, $query, $or = false)
287 | {
288 | $method = self::determineMethod('where', $or);
289 | $query->$method($column, 'LIKE', '%' . $filter);
290 | }
291 |
292 | /**
293 | * Query for items that contain a string.
294 | *
295 | * Ex: users?filters[favorite_cheese]=~cheddar
296 | *
297 | * @param string $column
298 | * @param string $filter
299 | * @param \Illuminate\Database\Eloquent\Builder $query
300 | * @param bool $or
301 | */
302 | protected static function contains($column, $filter, $query, $or = false)
303 | {
304 | $method = self::determineMethod('where', $or);
305 | $query->$method($column, 'LIKE', '%' . $filter . '%');
306 | }
307 |
308 | /**
309 | * Query for items with a value less than a filter.
310 | *
311 | * Ex: users?filters[lifetime_value]=<50
312 | *
313 | * @param string $column
314 | * @param string $filter
315 | * @param \Illuminate\Database\Eloquent\Builder $query
316 | * @param bool $or
317 | */
318 | protected static function lessThan($column, $filter, $query, $or = false)
319 | {
320 | $method = self::determineMethod('where', $or);
321 | $query->$method($column, '<', $filter);
322 | }
323 |
324 | /**
325 | * Query for items with a value greater than a filter.
326 | *
327 | * Ex: users?filters[lifetime_value]=>50
328 | *
329 | * @param string $column
330 | * @param string $filter
331 | * @param \Illuminate\Database\Eloquent\Builder $query
332 | * @param bool $or
333 | */
334 | protected static function greaterThan($column, $filter, $query, $or = false)
335 | {
336 | $method = self::determineMethod('where', $or);
337 | $query->$method($column, '>', $filter);
338 | }
339 |
340 | /**
341 | * Query for items with a value greater than or equal to a filter.
342 | *
343 | * Ex: users?filters[lifetime_value]=>=50
344 | *
345 | * @param string $column
346 | * @param string $filter
347 | * @param \Illuminate\Database\Eloquent\Builder $query
348 | * @param bool $or
349 | */
350 | protected static function greaterThanOrEquals($column, $filter, $query, $or = false)
351 | {
352 | $method = self::determineMethod('where', $or);
353 | $query->$method($column, '>=', $filter);
354 | }
355 |
356 | /**
357 | * Query for items with a value less than or equal to a filter.
358 | *
359 | * Ex: users?filters[lifetime_value]=<=50
360 | *
361 | * @param string $column
362 | * @param string $filter
363 | * @param \Illuminate\Database\Eloquent\Builder $query
364 | * @param bool $or
365 | */
366 | protected static function lessThanOrEquals($column, $filter, $query, $or = false)
367 | {
368 | $method = self::determineMethod('where', $or);
369 | $query->$method($column, '<=', $filter);
370 | }
371 |
372 | /**
373 | * Query for items with a value equal to a filter.
374 | *
375 | * Ex: users?filters[username]==Specific%20Username
376 | *
377 | * @param string $column
378 | * @param string $filter
379 | * @param \Illuminate\Database\Eloquent\Builder $query
380 | * @param bool $or
381 | */
382 | protected static function equals($column, $filter, $query, $or = false)
383 | {
384 | $method = self::determineMethod('where', $or);
385 |
386 | if ($filter === 'true' || $filter === 'false') {
387 | $filter = $filter === 'true';
388 | }
389 |
390 | $query->$method($column, '=', $filter);
391 | }
392 |
393 | /**
394 | * Query for items with a value not equal to a filter.
395 | *
396 | * Ex: users?filters[username]=!=common%20username
397 | *
398 | * @param string $column
399 | * @param string $filter
400 | * @param \Illuminate\Database\Eloquent\Builder $query
401 | * @param bool $or
402 | */
403 | protected static function notEquals($column, $filter, $query, $or = false)
404 | {
405 | $method = self::determineMethod('where', $or);
406 | $query->$method($column, '!=', $filter);
407 | }
408 |
409 | /**
410 | * Query for items that are either null or not null.
411 | *
412 | * Ex: users?filters[email]=NOT_NULL
413 | * Ex: users?filters[address]=NULL
414 | *
415 | * @param string $column
416 | * @param string $filter
417 | * @param \Illuminate\Database\Eloquent\Builder $query
418 | * @param bool $or
419 | */
420 | protected static function nullMethod($column, $filter, $query, $or = false)
421 | {
422 | if ($filter === 'NULL') {
423 | $method = self::determineMethod('whereNull', $or);
424 | $query->$method($column);
425 | } else {
426 | $method = self::determineMethod('whereNotNull', $or);
427 | $query->$method($column);
428 | }
429 | }
430 |
431 | /**
432 | * Query for items that are in a list.
433 | *
434 | * Ex: users?filters[id]=[1,5,10]
435 | *
436 | * @param string $column
437 | * @param string|array $filter
438 | * @param \Illuminate\Database\Eloquent\Builder $query
439 | * @param bool $or
440 | */
441 | protected static function in($column, $filter, $query, $or = false)
442 | {
443 | $method = self::determineMethod('whereIn', $or);
444 | $query->$method($column, $filter);
445 | }
446 |
447 | /**
448 | * Query for items that are not in a list.
449 | *
450 | * Ex: users?filters[id]=![1,5,10]
451 | *
452 | * @param string $column
453 | * @param string|array $filter
454 | * @param \Illuminate\Database\Eloquent\Builder $query
455 | * @param bool $or
456 | */
457 | protected static function notIn($column, $filter, $query, $or = false)
458 | {
459 | $method = self::determineMethod('whereNotIn', $or);
460 | $query->$method($column, $filter);
461 | }
462 |
463 | /**
464 | * Determine the token (if any) to use for the query
465 | *
466 | * @param string $filter
467 | *
468 | * @return bool|string
469 | */
470 | private static function determineTokenType($filter)
471 | {
472 | if (in_array(substr($filter, 0, 2), array_keys(self::$supported_tokens))) {
473 | // Two character token (<=, >=, etc)
474 | return substr($filter, 0, 2);
475 | } elseif (in_array($filter[0], array_keys(self::$supported_tokens))) {
476 | // Single character token (>, ^, $)
477 | return $filter[0];
478 | }
479 |
480 | // No token
481 | return false;
482 | }
483 |
484 | /**
485 | * Determine if a token should accept a scalar value
486 | *
487 | * @param string $token
488 | *
489 | * @return bool
490 | */
491 | private static function shouldBeScalar($token)
492 | {
493 | // Is token in array of tokens that can be non-scalar
494 | return ! in_array($token, self::$non_scalar_tokens);
495 | }
496 |
497 | /**
498 | * Parse a filter string and confirm that it has a scalar value if it should.
499 | *
500 | * @param string $token
501 | * @param string $filter
502 | *
503 | * @return array|bool
504 | */
505 | private static function cleanAndValidateFilter($token, $filter)
506 | {
507 | $filter_should_be_scalar = self::shouldBeScalar($token);
508 |
509 | // Format the filter, cutting off the trailing ']' if appropriate
510 | $filter = $filter_should_be_scalar ? explode(',', substr($filter, strlen($token))) :
511 | explode(',', substr($filter, strlen($token), -1));
512 |
513 | if ($filter_should_be_scalar) {
514 | if (count($filter) > 1) {
515 | return false;
516 | }
517 |
518 | // Set to first index if should be scalar
519 | $filter = $filter[0];
520 | }
521 |
522 | return $filter;
523 | }
524 |
525 | /**
526 | * Determine whether to apply a table prefix to prevent ambiguous columns
527 | *
528 | * @param $column
529 | *
530 | * @return string
531 | */
532 | private static function applyTablePrefix($column)
533 | {
534 | return is_null(self::$table_prefix) ? $column : self::$table_prefix . '.' . $column;
535 | }
536 |
537 | /**
538 | * Determine whether this an 'or' method or not
539 | *
540 | * @param string $base_name
541 | * @param bool $or
542 | *
543 | * @return string
544 | */
545 | private static function determineMethod($base_name, $or)
546 | {
547 | return $or ? camel_case('or_' . $base_name) : $base_name;
548 | }
549 | }
550 |
--------------------------------------------------------------------------------
/tests/FilterTest.php:
--------------------------------------------------------------------------------
1 | artisan->call(
29 | 'db:seed', [
30 | '--class' => FilterDataSeeder::class
31 | ]
32 | );
33 | }
34 |
35 | /**
36 | * Retrieve a query for the model
37 | *
38 | * @param string $model_class
39 | * @return \Illuminate\Database\Eloquent\Builder
40 | */
41 | private function getQuery($model_class)
42 | {
43 | return $model_class::query();
44 | }
45 |
46 | private function getModelColumns($model_class)
47 | {
48 | $temp_instance = new $model_class;
49 |
50 | return Schema::getColumnListing($temp_instance->getTable());
51 | }
52 |
53 | public function testItModifiesQuery()
54 | {
55 | $this->assertEquals(User::all()->count(), $this->user_count);
56 | $filters = ['name' => '^lskywalker'];
57 |
58 | $model = User::class;
59 | $query = $this->getQuery($model);
60 | $original_query = clone $query;
61 | $columns = $this->getModelColumns($model);
62 |
63 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
64 |
65 | $this->assertNotSame($original_query, $query);
66 | }
67 |
68 | public function testItStartsWith()
69 | {
70 | $this->assertEquals(User::all()->count(), $this->user_count);
71 | $filters = ['username' => '^lskywalker'];
72 |
73 | $model = User::class;
74 | $query = $this->getQuery($model);
75 | $columns = $this->getModelColumns($model);
76 |
77 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
78 |
79 | $results = $query->get();
80 |
81 | $this->assertEquals(1, count($results));
82 | $this->assertEquals('lskywalker@galaxyfarfaraway.com', $results->first()->username);
83 | }
84 |
85 | public function testItFiltersOrStartsWith()
86 | {
87 | $this->assertEquals(User::all()->count(), $this->user_count);
88 | $filters = [
89 | 'username' => '^lskywalker',
90 | 'or' => ['username' => '^solocup']
91 | ];
92 |
93 | $model = User::class;
94 | $query = $this->getQuery($model);
95 | $columns = $this->getModelColumns($model);
96 |
97 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
98 |
99 | $results = $query->get();
100 |
101 | $this->assertEquals(2, count($results));
102 |
103 | foreach ($results as $result){
104 | $this->assertTrue(in_array($result->username, ['lskywalker@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com']));
105 | }
106 | }
107 |
108 | public function testItEndsWith()
109 | {
110 | $this->assertEquals(User::all()->count(), $this->user_count);
111 | $filters = ['name' => '$gana'];
112 |
113 | $model = User::class;
114 | $query = $this->getQuery($model);
115 | $columns = $this->getModelColumns($model);
116 |
117 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
118 |
119 | $results = $query->get();
120 |
121 | $this->assertEquals(1, count($results));
122 | $this->assertEquals('lorgana@galaxyfarfaraway.com', $results->first()->username);
123 | }
124 |
125 | public function testItFiltersOrEndsWith()
126 | {
127 | $this->assertEquals(User::all()->count(), $this->user_count);
128 | $filters = [
129 | 'name' => '$gana',
130 | 'or' => ['name' => '$olo']
131 | ];
132 |
133 | $model = User::class;
134 | $query = $this->getQuery($model);
135 | $columns = $this->getModelColumns($model);
136 |
137 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
138 |
139 | $results = $query->get();
140 |
141 | $this->assertEquals(2, count($results));
142 | foreach ($results as $result){
143 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com']));
144 | }
145 | }
146 |
147 | public function testItContains()
148 | {
149 | $this->assertEquals(User::all()->count(), $this->user_count);
150 | $filters = ['username' => '~clava'];
151 |
152 | $model = User::class;
153 | $query = $this->getQuery($model);
154 | $columns = $this->getModelColumns($model);
155 |
156 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
157 |
158 | $results = $query->get();
159 |
160 | $this->assertEquals(1, count($results));
161 | $this->assertEquals('chewbaclava@galaxyfarfaraway.com', $results->first()->username);
162 | }
163 |
164 | public function testItFiltersOrContains()
165 | {
166 | $this->assertEquals(User::all()->count(), $this->user_count);
167 | $filters = [
168 | 'username' => '~skywalker',
169 | 'or' => ['username' => '~clava']
170 | ];
171 |
172 | $model = User::class;
173 | $query = $this->getQuery($model);
174 | $columns = $this->getModelColumns($model);
175 |
176 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
177 |
178 | $results = $query->get();
179 |
180 | $this->assertEquals(2, count($results));
181 | foreach ($results as $result){
182 | $this->assertTrue(in_array($result->username, ['lskywalker@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
183 | }
184 | }
185 |
186 | public function testItIsLessThan()
187 | {
188 | $this->assertEquals(User::all()->count(), $this->user_count);
189 | $filters = ['times_captured' => '<1'];
190 |
191 | $model = User::class;
192 | $query = $this->getQuery($model);
193 | $columns = $this->getModelColumns($model);
194 |
195 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
196 |
197 | $results = $query->get();
198 |
199 | $this->assertEquals(1, count($results));
200 | $this->assertEquals('chewbaclava@galaxyfarfaraway.com', $results->first()->username);
201 | }
202 |
203 | public function testItFiltersOrIsLessThan()
204 | {
205 | $this->assertEquals(User::all()->count(), $this->user_count);
206 | $filters = [
207 | 'times_captured' => '<1',
208 | 'or' => ['times_captured' => '<3']
209 | ];
210 |
211 | $model = User::class;
212 | $query = $this->getQuery($model);
213 | $columns = $this->getModelColumns($model);
214 |
215 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
216 |
217 | $results = $query->get();
218 |
219 | $this->assertEquals(2, count($results));
220 | foreach ($results as $result){
221 | $this->assertTrue(in_array($result->username, ['solocup@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
222 | }
223 | }
224 |
225 | public function testItIsGreaterThan()
226 | {
227 | $this->assertEquals(User::all()->count(), $this->user_count);
228 | $filters = ['hands' => '>1'];
229 |
230 | $model = User::class;
231 | $query = $this->getQuery($model);
232 | $columns = $this->getModelColumns($model);
233 |
234 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
235 |
236 | $results = $query->get();
237 |
238 | $this->assertEquals(2, count($results));
239 | foreach ($results as $result){
240 | $this->assertTrue(in_array($result->username, ['solocup@galaxyfarfaraway.com', 'lorgana@galaxyfarfaraway.com']));
241 | }
242 | }
243 |
244 | public function testItFiltersOrIsGreaterThan()
245 | {
246 | $this->assertEquals(User::all()->count(), $this->user_count);
247 | $filters = [
248 | 'hands' => '>1',
249 | 'or' => ['hands' => '>0']
250 | ];
251 |
252 | $model = User::class;
253 | $query = $this->getQuery($model);
254 | $columns = $this->getModelColumns($model);
255 |
256 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
257 |
258 | $results = $query->get();
259 |
260 | $this->assertEquals(3, count($results));
261 | foreach ($results as $result){
262 | $this->assertTrue(in_array($result->username, ['solocup@galaxyfarfaraway.com', 'lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
263 | }
264 | }
265 |
266 | public function testItIsLessThanOrEquals()
267 | {
268 | $this->assertEquals(User::all()->count(), $this->user_count);
269 | $filters = ['hands' => '<=1'];
270 |
271 | $model = User::class;
272 | $query = $this->getQuery($model);
273 | $columns = $this->getModelColumns($model);
274 |
275 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
276 |
277 | $results = $query->get();
278 |
279 | $this->assertEquals(2, count($results));
280 | foreach ($results as $result){
281 | $this->assertTrue(in_array($result->username, ['chewbaclava@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
282 | }
283 | }
284 |
285 | public function testItFiltersOrIsLessThanOrEquals()
286 | {
287 | $this->assertEquals(User::all()->count(), $this->user_count);
288 | $filters = [
289 | 'times_captured' => '<=2',
290 | 'or' => ['times_captured' => '<=5']
291 | ];
292 |
293 | $model = User::class;
294 | $query = $this->getQuery($model);
295 | $columns = $this->getModelColumns($model);
296 |
297 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
298 |
299 | $results = $query->get();
300 |
301 | $this->assertEquals(3, count($results));
302 | foreach ($results as $result){
303 | $this->assertTrue(in_array($result->username, ['chewbaclava@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com']));
304 | }
305 | }
306 |
307 | public function testItIsGreaterThanOrEquals()
308 | {
309 | $this->assertEquals(User::all()->count(), $this->user_count);
310 | $filters = [
311 | 'times_captured' => '>=5',
312 | ];
313 |
314 | $model = User::class;
315 | $query = $this->getQuery($model);
316 | $columns = $this->getModelColumns($model);
317 |
318 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
319 |
320 | $results = $query->get();
321 |
322 | $this->assertEquals(1, count($results));
323 | foreach ($results as $result){
324 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com']));
325 | }
326 | }
327 |
328 | public function testItFiltersOrIsGreaterThanOrEquals()
329 | {
330 | $this->assertEquals(User::all()->count(), $this->user_count);
331 | $filters = [
332 | 'times_captured' => '>=5',
333 | 'or' => ['times_captured' => '>=3']
334 | ];
335 |
336 | $model = User::class;
337 | $query = $this->getQuery($model);
338 | $columns = $this->getModelColumns($model);
339 |
340 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
341 |
342 | $results = $query->get();
343 |
344 | $this->assertEquals(2, count($results));
345 | foreach ($results as $result){
346 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
347 | }
348 | }
349 |
350 | public function testItEqualsString()
351 | {
352 | $this->assertEquals(User::all()->count(), $this->user_count);
353 | $filters = [
354 | 'username' => '=lskywalker@galaxyfarfaraway.com',
355 | ];
356 |
357 | $model = User::class;
358 | $query = $this->getQuery($model);
359 | $columns = $this->getModelColumns($model);
360 |
361 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
362 |
363 | $results = $query->get();
364 |
365 | $this->assertEquals(1, count($results));
366 | foreach ($results as $result){
367 | $this->assertTrue(in_array($result->username, ['lskywalker@galaxyfarfaraway.com']));
368 | }
369 | }
370 |
371 | public function testItEqualsInt()
372 | {
373 | $this->assertEquals(User::all()->count(), $this->user_count);
374 | $filters = [
375 | 'times_captured' => '=6',
376 | ];
377 |
378 | $model = User::class;
379 | $query = $this->getQuery($model);
380 | $columns = $this->getModelColumns($model);
381 |
382 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
383 |
384 | $results = $query->get();
385 |
386 | $this->assertEquals(1, count($results));
387 | foreach ($results as $result){
388 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com']));
389 | }
390 | }
391 |
392 | public function testItFiltersOrEqualsString()
393 | {
394 | $this->assertEquals(User::all()->count(), $this->user_count);
395 | $filters = [
396 | 'username' => '=lskywalker@galaxyfarfaraway.com',
397 | 'or' => ['username' => '=lorgana@galaxyfarfaraway.com']
398 | ];
399 |
400 | $model = User::class;
401 | $query = $this->getQuery($model);
402 | $columns = $this->getModelColumns($model);
403 |
404 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
405 |
406 | $results = $query->get();
407 |
408 | $this->assertEquals(2, count($results));
409 | foreach ($results as $result){
410 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
411 | }
412 | }
413 |
414 | public function testItFiltersOrEqualsInt()
415 | {
416 | $this->assertEquals(User::all()->count(), $this->user_count);
417 | $filters = [
418 | 'times_captured' => '=4',
419 | 'or' => ['times_captured' => '=6']
420 | ];
421 |
422 | $model = User::class;
423 | $query = $this->getQuery($model);
424 | $columns = $this->getModelColumns($model);
425 |
426 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
427 |
428 | $results = $query->get();
429 |
430 | $this->assertEquals(2, count($results));
431 | foreach ($results as $result){
432 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
433 | }
434 | }
435 |
436 | public function testItNotEqualsString()
437 | {
438 | $this->assertEquals(User::all()->count(), $this->user_count);
439 | $filters = ['username' => '!=lorgana@galaxyfarfaraway.com'];
440 |
441 | $model = User::class;
442 | $query = $this->getQuery($model);
443 | $columns = $this->getModelColumns($model);
444 |
445 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
446 |
447 | $results = $query->get();
448 |
449 | $this->assertEquals(3, count($results));
450 | foreach ($results as $result){
451 | $this->assertTrue($result->username !== 'lorgana@galaxyfarfaraway.com');
452 | }
453 | }
454 |
455 | public function testItNotEqualsInt()
456 | {
457 | $this->assertEquals(User::all()->count(), $this->user_count);
458 | $filters = ['times_captured' => '!=4'];
459 |
460 | $model = User::class;
461 | $query = $this->getQuery($model);
462 | $columns = $this->getModelColumns($model);
463 |
464 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
465 |
466 | $results = $query->get();
467 |
468 | $this->assertEquals(3, count($results));
469 | foreach ($results as $result){
470 | $this->assertTrue($result->username !== 'lskywalker@galaxyfarfaraway.com');
471 | }
472 | }
473 |
474 | public function testItFiltersOrNotEqualString()
475 | {
476 | $this->assertEquals(User::all()->count(), $this->user_count);
477 | $filters = [
478 | 'username' => '=lorgana@galaxyfarfaraway.com',
479 | 'or' => ['username' => '!=lskywalker@galaxyfarfaraway.com']
480 | ];
481 |
482 | $model = User::class;
483 | $query = $this->getQuery($model);
484 | $columns = $this->getModelColumns($model);
485 |
486 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
487 |
488 | $results = $query->get();
489 |
490 | $this->assertEquals(3, count($results));
491 | foreach ($results as $result){
492 | // The only one we shouldn't get is lskywalker@galaxyfarfaraway.com'
493 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
494 | }
495 | }
496 |
497 | public function testItFiltersOrNotEqualInt()
498 | {
499 | $this->assertEquals(User::all()->count(), $this->user_count);
500 | $filters = [
501 | 'times_captured' => '=6',
502 | 'or' => ['times_captured' => '!=4']
503 | ];
504 |
505 | $model = User::class;
506 | $query = $this->getQuery($model);
507 | $columns = $this->getModelColumns($model);
508 |
509 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
510 |
511 | $results = $query->get();
512 |
513 | $this->assertEquals(3, count($results));
514 | foreach ($results as $result){
515 | // The only one we shouldn't get is lskywalker@galaxyfarfaraway.com'
516 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
517 | }
518 | }
519 |
520 | public function testItNotNull()
521 | {
522 | $this->assertEquals(User::all()->count(), $this->user_count);
523 | $filters = ['occupation' => 'NOT_NULL'];
524 |
525 | $model = User::class;
526 | $query = $this->getQuery($model);
527 | $columns = $this->getModelColumns($model);
528 |
529 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
530 |
531 | $results = $query->get();
532 |
533 | $this->assertEquals(3, count($results));
534 | foreach ($results as $result){
535 | $this->assertTrue(in_array($result->username, ['lskywalker@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
536 | }
537 | }
538 |
539 | public function testItFiltersOrNotNull()
540 | {
541 | $this->assertEquals(User::all()->count(), $this->user_count);
542 | $filters = [
543 | 'username' => '~lskywalker',
544 | 'or' => ['occupation' => 'NOT_NULL']
545 | ];
546 |
547 | $model = User::class;
548 | $query = $this->getQuery($model);
549 | $columns = $this->getModelColumns($model);
550 |
551 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
552 |
553 | $results = $query->get();
554 |
555 | $this->assertEquals(3, count($results));
556 | foreach ($results as $result){
557 | $this->assertTrue(in_array($result->username, ['lskywalker@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
558 | }
559 | }
560 |
561 | public function testItNull()
562 | {
563 | $this->assertEquals(User::all()->count(), $this->user_count);
564 | $filters = [
565 | 'occupation' => 'NULL',
566 | ];
567 |
568 | $model = User::class;
569 | $query = $this->getQuery($model);
570 | $columns = $this->getModelColumns($model);
571 |
572 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
573 |
574 | $results = $query->get();
575 |
576 | $this->assertEquals(1, count($results));
577 | foreach ($results as $result){
578 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com',]));
579 | }
580 | }
581 |
582 | public function testItFiltersOrNull()
583 | {
584 | $this->assertEquals(User::all()->count(), $this->user_count);
585 | $filters = [
586 | 'occupation' => '=Jedi',
587 | 'or' => [
588 | 'occupation' => 'NULL',
589 | ],
590 | ];
591 |
592 | $model = User::class;
593 | $query = $this->getQuery($model);
594 | $columns = $this->getModelColumns($model);
595 |
596 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
597 |
598 | $results = $query->get();
599 |
600 | $this->assertEquals(2, count($results));
601 | foreach ($results as $result){
602 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
603 | }
604 | }
605 |
606 | public function testItInString()
607 | {
608 | $this->assertEquals(User::all()->count(), $this->user_count);
609 | $filters = ['name' => '[Chewbacca,Leia Organa,Luke Skywalker]'];
610 |
611 | $model = User::class;
612 | $query = $this->getQuery($model);
613 | $columns = $this->getModelColumns($model);
614 |
615 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
616 |
617 | $results = $query->get();
618 |
619 | $this->assertEquals(3, count($results));
620 | foreach ($results as $result){
621 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
622 | }
623 | }
624 |
625 | public function testItInInt()
626 | {
627 | $this->assertEquals(User::all()->count(), $this->user_count);
628 | $filters = ['times_captured' => '[0,4,6]'];
629 |
630 | $model = User::class;
631 | $query = $this->getQuery($model);
632 | $columns = $this->getModelColumns($model);
633 |
634 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
635 |
636 | $results = $query->get();
637 |
638 | $this->assertEquals(3, count($results));
639 | foreach ($results as $result){
640 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
641 | }
642 | }
643 |
644 | public function testItFiltersOrInString()
645 | {
646 | $this->assertEquals(User::all()->count(), $this->user_count);
647 | $filters = [
648 | 'name' => '[Chewbacca,Luke Skywalker]',
649 | 'or' => ['name' => '[Luke Skywalker,Leia Organa]']
650 | ];
651 |
652 | $model = User::class;
653 | $query = $this->getQuery($model);
654 | $columns = $this->getModelColumns($model);
655 |
656 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
657 |
658 | $results = $query->get();
659 |
660 | $this->assertEquals(3, count($results));
661 | foreach ($results as $result){
662 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
663 | }
664 | }
665 |
666 | public function testItFiltersOrInInt()
667 | {
668 | $this->assertEquals(User::all()->count(), $this->user_count);
669 | $filters = [
670 | 'times_captured' => '[0,4]',
671 | 'or' => ['times_captured' => '[4,6]']
672 | ];
673 |
674 | $model = User::class;
675 | $query = $this->getQuery($model);
676 | $columns = $this->getModelColumns($model);
677 |
678 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
679 |
680 | $results = $query->get();
681 |
682 | $this->assertEquals(3, count($results));
683 | foreach ($results as $result){
684 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com', 'chewbaclava@galaxyfarfaraway.com']));
685 | }
686 | }
687 |
688 | public function testItNotInString()
689 | {
690 | $this->assertEquals(User::all()->count(), $this->user_count);
691 | $filters = [
692 | 'name' => '![Leia Organa,Chewbacca]',
693 | ];
694 |
695 | $model = User::class;
696 | $query = $this->getQuery($model);
697 | $columns = $this->getModelColumns($model);
698 |
699 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
700 |
701 | $results = $query->get();
702 |
703 | $this->assertEquals(2, count($results));
704 | foreach ($results as $result){
705 | $this->assertTrue(in_array($result->username, ['solocup@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com',]));
706 | }
707 | }
708 |
709 | public function testItNotInInt()
710 | {
711 | $this->assertEquals(User::all()->count(), $this->user_count);
712 | $filters = [
713 | 'times_captured' => '![6,0]',
714 | ];
715 |
716 | $model = User::class;
717 | $query = $this->getQuery($model);
718 | $columns = $this->getModelColumns($model);
719 |
720 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
721 |
722 | $results = $query->get();
723 |
724 | $this->assertEquals(2, count($results));
725 | foreach ($results as $result){
726 | $this->assertTrue(in_array($result->username, ['solocup@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com',]));
727 | }
728 | }
729 |
730 | public function testItFiltersOrNotInString()
731 | {
732 | $this->assertEquals(User::all()->count(), $this->user_count);
733 | $filters = [
734 | 'name' => '![Leia Organa,Chewbacca]',
735 | 'or' => [
736 | 'name' => '![Leia Organa,Chewbacca,Han Solo]',
737 | ]
738 | ];
739 |
740 | $model = User::class;
741 | $query = $this->getQuery($model);
742 | $columns = $this->getModelColumns($model);
743 |
744 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
745 |
746 | $results = $query->get();
747 |
748 | $this->assertEquals(2, count($results));
749 | foreach ($results as $result){
750 | $this->assertTrue(in_array($result->username, ['solocup@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
751 | }
752 | }
753 |
754 | public function testItFiltersOrNotInInt()
755 | {
756 | $this->assertEquals(User::all()->count(), $this->user_count);
757 | $filters = [
758 | 'times_captured' => '![6,0]',
759 | 'or' => [
760 | 'times_captured' => '![0,4,6]'
761 | ]
762 | ];
763 |
764 | $model = User::class;
765 | $query = $this->getQuery($model);
766 | $columns = $this->getModelColumns($model);
767 |
768 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
769 |
770 | $results = $query->get();
771 |
772 | $this->assertEquals(2, count($results));
773 | foreach ($results as $result){
774 | $this->assertTrue(in_array($result->username, ['solocup@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
775 | }
776 | }
777 |
778 | public function testItFiltersNestedRelationships()
779 | {
780 | $this->assertEquals(User::all()->count(), $this->user_count);
781 | $filters = ['profile.favorite_cheese' => '~Gou'];
782 |
783 | $model = User::class;
784 | $query = $this->getQuery($model);
785 | $columns = $this->getModelColumns($model);
786 |
787 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
788 |
789 | $results = $query->get();
790 |
791 | $this->assertEquals(1, count($results));
792 | foreach ($results as $result){
793 | $this->assertTrue(in_array($result->username, ['lskywalker@galaxyfarfaraway.com']));
794 | }
795 | }
796 |
797 | public function testItProperlyDeterminesScalarFilters()
798 | {
799 | $this->assertEquals(User::all()->count(), $this->user_count);
800 | $filters = ['name' => '=Leia Organa,Luke Skywalker'];
801 |
802 | $model = User::class;
803 | $query = $this->getQuery($model);
804 | $columns = $this->getModelColumns($model);
805 |
806 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
807 |
808 | $results = $query->get();
809 |
810 | $this->assertEquals(4, count($results)); // It does not filter anything because this is a scalar filter
811 | }
812 |
813 | public function testItFiltersFalse()
814 | {
815 | $this->assertEquals(User::all()->count(), $this->user_count);
816 | $filters = ['profile.is_human' => 'false'];
817 |
818 | $model = User::class;
819 | $query = $this->getQuery($model);
820 | $columns = $this->getModelColumns($model);
821 |
822 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
823 |
824 | $results = $query->get();
825 |
826 | $this->assertEquals(1, count($results));
827 | foreach ($results as $result){
828 | $this->assertTrue(in_array($result->username, ['chewbaclava@galaxyfarfaraway.com']));
829 | }
830 | }
831 |
832 | public function testItFiltersNestedTrue()
833 | {
834 | $this->assertEquals(User::all()->count(), $this->user_count);
835 | $filters = ['profile.is_human' => 'true'];
836 |
837 | $model = User::class;
838 | $query = $this->getQuery($model);
839 | $columns = $this->getModelColumns($model);
840 |
841 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
842 |
843 | $results = $query->get();
844 |
845 | $this->assertEquals(3, count($results));
846 | foreach ($results as $result){
847 | $this->assertTrue(! in_array($result->username, ['chewbaclava@galaxyfarfaraway.com']));
848 | }
849 | }
850 |
851 | public function testItFiltersNestedNull()
852 | {
853 | $this->assertEquals(User::all()->count(), $this->user_count);
854 | $filters = ['profile.favorite_fruit' => 'NULL'];
855 |
856 | $model = User::class;
857 | $query = $this->getQuery($model);
858 | $columns = $this->getModelColumns($model);
859 |
860 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
861 |
862 | $results = $query->get();
863 |
864 | $this->assertEquals(2, count($results));
865 | foreach ($results as $result){
866 | $this->assertTrue(in_array($result->username, ['chewbaclava@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com']));
867 | }
868 | }
869 |
870 | /**
871 | * Check to see if filtering by id works with a many to many relationship.
872 | */
873 | public function testItFiltersNestedBelongsToManyRelationships()
874 | {
875 | $this->assertEquals(User::all()->count(), $this->user_count);
876 | $filters = ['posts.tags.label' => '=#mysonistheworst'];
877 |
878 | $model = User::class;
879 | $query = $this->getQuery($model);
880 | $columns = $this->getModelColumns($model);
881 |
882 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
883 |
884 | $results = $query->get();
885 |
886 | $this->assertEquals(2, count($results));
887 | foreach ($results as $result){
888 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com']));
889 | }
890 | }
891 |
892 | public function testItFiltersNestedConjuctions()
893 | {
894 | $this->assertEquals(User::all()->count(), $this->user_count);
895 | $filters = [
896 | 'username' => '^lskywalker',
897 | 'or' => [
898 | 'name' => '$gana',
899 | 'and' => [
900 | 'profile.favorite_cheese' => '=Provolone',
901 | 'username' => '$gana@galaxyfarfaraway.com'
902 | ],
903 | 'or' => [
904 | 'username' => '=solocup@galaxyfarfaraway.com'
905 | ]
906 | ]
907 | ];
908 |
909 | $model = User::class;
910 | $query = $this->getQuery($model);
911 | $columns = $this->getModelColumns($model);
912 |
913 | Filter::applyQueryFilters($query, $filters, $columns, (new $model)->getTable());
914 |
915 | $results = $query->get();
916 |
917 | $this->assertEquals(3, count($results));
918 | foreach ($results as $result){
919 | $this->assertTrue(in_array($result->username, ['lorgana@galaxyfarfaraway.com', 'solocup@galaxyfarfaraway.com', 'lskywalker@galaxyfarfaraway.com']));
920 | }
921 | }
922 |
923 | public function testItCanIntersectAllowedFilters()
924 | {
925 | $filters = [
926 | 'username' => '^lskywalker',
927 | 'or' => [
928 | 'name' => '$gana',
929 | 'and' => [
930 | 'profile.favorite_cheese' => '=Provolone',
931 | 'username' => '$gana@galaxyfarfaraway.com'
932 | ],
933 | 'or' => [
934 | 'username' => '=solocup@galaxyfarfaraway.com',
935 | 'and' => [
936 | 'profile.least_favorite_cheese' => '~gouda'
937 | ]
938 | ]
939 | ]
940 | ];
941 |
942 | $allowed = [
943 | 'username' => true,
944 | 'profile.favorite_cheese' => true,
945 | ];
946 |
947 | $this->assertSame([
948 | 'username' => '^lskywalker',
949 | 'or' => [
950 | 'and' => [
951 | 'profile.favorite_cheese' => '=Provolone',
952 | 'username' => '$gana@galaxyfarfaraway.com'
953 | ],
954 | 'or' => [
955 | 'username' => '=solocup@galaxyfarfaraway.com',
956 | ]
957 | ]
958 | ], Filter::intersectAllowedFilters($filters, $allowed));
959 |
960 | $filters = [
961 | 'username' => '^lskywalker',
962 | 'or' => [
963 | 'name' => '$gana',
964 | 'and' => [
965 | 'profile.favorite_cheese' => '=Provolone',
966 | 'username' => '$gana@galaxyfarfaraway.com'
967 | ],
968 | 'or' => [
969 | 'username' => '=solocup@galaxyfarfaraway.com',
970 | 'and' => [
971 | 'profile.least_favorite_cheese' => '~gouda'
972 | ]
973 | ]
974 | ]
975 | ];
976 |
977 | $allowed = [
978 | // None
979 | ];
980 |
981 | $this->assertSame([], Filter::intersectAllowedFilters($filters, $allowed));
982 |
983 | $filters = [
984 | 'username' => '^lskywalker',
985 | 'or' => [
986 | 'name' => '$gana',
987 | 'and' => [
988 | 'profile.favorite_cheese' => '=Provolone',
989 | 'username' => '$gana@galaxyfarfaraway.com'
990 | ],
991 | 'or' => [
992 | 'username' => '=solocup@galaxyfarfaraway.com',
993 | 'and' => [
994 | 'profile.least_favorite_cheese' => '~gouda'
995 | ]
996 | ]
997 | ]
998 | ];
999 |
1000 | $allowed = [
1001 | 'username' => true,
1002 | 'profile.favorite_cheese' => true,
1003 | 'profile.least_favorite_cheese' => true,
1004 | 'name' => true,
1005 | ];
1006 |
1007 | $this->assertSame([
1008 | 'username' => '^lskywalker',
1009 | 'or' => [
1010 | 'name' => '$gana',
1011 | 'and' => [
1012 | 'profile.favorite_cheese' => '=Provolone',
1013 | 'username' => '$gana@galaxyfarfaraway.com'
1014 | ],
1015 | 'or' => [
1016 | 'username' => '=solocup@galaxyfarfaraway.com',
1017 | 'and' => [
1018 | 'profile.least_favorite_cheese' => '~gouda'
1019 | ]
1020 | ]
1021 | ]
1022 | ], Filter::intersectAllowedFilters($filters, $allowed));
1023 | }
1024 | }
1025 |
--------------------------------------------------------------------------------
/src/EloquentRepository.php:
--------------------------------------------------------------------------------
1 | model_class = $model_class;
145 |
146 | /** @var \Illuminate\Database\Eloquent\Model|\Fuzz\MagicBox\Contracts\MagicBoxResource $instance */
147 | $instance = new $model_class;
148 |
149 | // @todo use set methods
150 | $this->setFillable($instance->getRepositoryFillable());
151 | $this->setIncludable($instance->getRepositoryIncludable());
152 | $this->setFilterable($instance->getRepositoryFilterable());
153 |
154 | $this->key_name = $instance->getKeyName();
155 |
156 | return $this;
157 | }
158 |
159 | /**
160 | * Get the PK name
161 | *
162 | * @return string
163 | */
164 | public function getKeyName(): string
165 | {
166 | return $this->key_name;
167 | }
168 |
169 | /**
170 | * Determine if the model exists
171 | *
172 | * @return bool
173 | */
174 | public function exists(): bool
175 | {
176 | return array_key_exists($this->getKeyName(), $this->getInput());
177 | }
178 |
179 | /**
180 | * Get the model class.
181 | *
182 | * @return string
183 | */
184 | public function getModelClass(): string
185 | {
186 | return $this->model_class;
187 | }
188 |
189 | /**
190 | * Set input manually.
191 | *
192 | * @param array $input
193 | * @return \Fuzz\MagicBox\Contracts\Repository
194 | */
195 | public function setInput(array $input): Repository
196 | {
197 | $this->input = $input;
198 |
199 | return $this;
200 | }
201 |
202 | /**
203 | * Get input.
204 | *
205 | * @return array
206 | */
207 | public function getInput(): array
208 | {
209 | return $this->input;
210 | }
211 |
212 | /**
213 | * Set eager load manually.
214 | *
215 | * @param array $eager_loads
216 | * @return \Fuzz\MagicBox\Contracts\Repository
217 | */
218 | public function setEagerLoads(array $eager_loads): Repository
219 | {
220 | $this->eager_loads = $eager_loads;
221 |
222 | return $this;
223 | }
224 |
225 | /**
226 | * Get eager loads.
227 | *
228 | * @return array
229 | */
230 | public function getEagerLoads(): array
231 | {
232 | return $this->eager_loads;
233 | }
234 |
235 | /**
236 | * Get the eager load depth property.
237 | *
238 | * @return int
239 | */
240 | public function getDepthRestriction(): int
241 | {
242 | return $this->depth_restriction;
243 | }
244 |
245 | /**
246 | * Set the eager load depth property.
247 | * This will limit how deep relationships can be included.
248 | *
249 | * @param int $depth
250 | *
251 | * @return \Fuzz\MagicBox\Contracts\Repository
252 | */
253 | public function setDepthRestriction($depth): Repository
254 | {
255 | $this->depth_restriction = $depth;
256 |
257 | return $this;
258 | }
259 |
260 | /**
261 | * Set filters manually.
262 | *
263 | * @param array $filters
264 | * @return \Fuzz\MagicBox\Contracts\Repository
265 | */
266 | public function setFilters(array $filters): Repository
267 | {
268 | $this->filters = $filters;
269 |
270 | return $this;
271 | }
272 |
273 | /**
274 | * Get filters.
275 | *
276 | * @return array
277 | */
278 | public function getFilters(): array
279 | {
280 | return $this->filters;
281 | }
282 |
283 | /**
284 | * Add filters to already existing filters without overwriting them.
285 | *
286 | * @param array $filters
287 | * @return \Fuzz\MagicBox\Contracts\Repository
288 | */
289 | public function addFilters(array $filters): Repository
290 | {
291 | foreach ($filters as $key => $value) {
292 | $this->addFilter($key, $value);
293 | }
294 |
295 | return $this;
296 | }
297 |
298 | /**
299 | * Add a single filter to already existing filters without overwriting them.
300 | *
301 | * @param string $key
302 | * @param string $value
303 | * @return \Fuzz\MagicBox\Contracts\Repository
304 | */
305 | public function addFilter(string $key, string $value): Repository
306 | {
307 | $this->filters[$key] = $value;
308 |
309 | return $this;
310 | }
311 |
312 |
313 | /**
314 | * Get group by.
315 | *
316 | * @return array
317 | */
318 | public function getGroupBy(): array
319 | {
320 | return $this->group_by;
321 | }
322 |
323 | /**
324 | * Set group by manually.
325 | *
326 | * @param array $group_by
327 | *
328 | * @return \Fuzz\MagicBox\Contracts\Repository
329 | */
330 | public function setGroupBy(array $group_by): Repository
331 | {
332 | $this->group_by = $group_by;
333 |
334 | return $this;
335 | }
336 |
337 | /**
338 | * @return array
339 | */
340 | public function getAggregate(): array
341 | {
342 | return $this->aggregate;
343 | }
344 |
345 | /**
346 | * Set aggregate functions.
347 | *
348 | * @param array $aggregate
349 | *
350 | * @return \Fuzz\MagicBox\Contracts\Repository
351 | */
352 | public function setAggregate(array $aggregate): Repository
353 | {
354 | $this->aggregate = $aggregate;
355 |
356 | return $this;
357 | }
358 |
359 | /**
360 | * Set sort order manually.
361 | *
362 | * @param array $sort_order
363 | * @return \Fuzz\MagicBox\Contracts\Repository
364 | */
365 | public function setSortOrder(array $sort_order): Repository
366 | {
367 | $this->sort_order = $sort_order;
368 |
369 | return $this;
370 | }
371 |
372 | /**
373 | * Get sort order.
374 | *
375 | * @return array
376 | */
377 | public function getSortOrder(): array
378 | {
379 | return $this->sort_order;
380 | }
381 |
382 | /**
383 | * Add a single modifier
384 | *
385 | * @param \Closure $modifier
386 | *
387 | * @return \Fuzz\MagicBox\Contracts\Repository
388 | */
389 | public function addModifier(Closure $modifier): Repository
390 | {
391 | $this->modifiers[] = $modifier;
392 |
393 | return $this;
394 | }
395 |
396 | /**
397 | * Set modifiers.
398 | *
399 | * @param array $modifiers
400 | * @return \Fuzz\MagicBox\Contracts\Repository
401 | */
402 | public function setModifiers(array $modifiers): Repository
403 | {
404 | $this->modifiers = $modifiers;
405 |
406 | return $this;
407 | }
408 |
409 | /**
410 | * Get modifiers.
411 | *
412 | * @return array
413 | */
414 | public function getModifiers(): array
415 | {
416 | return $this->modifiers;
417 | }
418 |
419 | /**
420 | * Set the fillable array
421 | *
422 | * @param array $fillable
423 | *
424 | * @return \Fuzz\MagicBox\Contracts\Repository
425 | */
426 | public function setFillable(array $fillable): Repository
427 | {
428 | if ($fillable === self::ALLOW_ALL) {
429 | $this->fillable = self::ALLOW_ALL;
430 |
431 | return $this;
432 | }
433 |
434 | // Reset fillable
435 | $this->fillable = [];
436 |
437 | foreach ($fillable as $allowed_field) {
438 | $this->fillable[$allowed_field] = true;
439 | }
440 |
441 | return $this;
442 | }
443 |
444 | /**
445 | * Get the fillable attributes
446 | *
447 | * @param bool $assoc
448 | *
449 | * @return array
450 | */
451 | public function getFillable(bool $assoc = false): array
452 | {
453 | if ($this->fillable === self::ALLOW_ALL) {
454 | return self::ALLOW_ALL;
455 | }
456 |
457 | return $assoc ? $this->fillable : array_keys($this->fillable);
458 | }
459 |
460 | /**
461 | * Add a fillable attribute
462 | *
463 | * @param string $fillable
464 | *
465 | * @return \Fuzz\MagicBox\Contracts\Repository
466 | */
467 | public function addFillable(string $fillable): Repository
468 | {
469 | $this->fillable[$fillable] = true;
470 |
471 | return $this;
472 | }
473 |
474 | /**
475 | * Add many fillable fields
476 | *
477 | * @param array $fillable
478 | *
479 | * @return \Fuzz\MagicBox\Contracts\Repository
480 | */
481 | public function addManyFillable(array $fillable): Repository
482 | {
483 | foreach ($fillable as $allowed_field) {
484 | $this->addFillable($allowed_field);
485 | }
486 |
487 | return $this;
488 | }
489 |
490 | /**
491 | * Remove a fillable attribute
492 | *
493 | * @param string $fillable
494 | *
495 | * @return \Fuzz\MagicBox\Contracts\Repository
496 | */
497 | public function removeFillable(string $fillable): Repository
498 | {
499 | unset($this->fillable[$fillable]);
500 |
501 | return $this;
502 | }
503 |
504 | /**
505 | * Remove many fillable fields
506 | *
507 | * @param array $fillable
508 | *
509 | * @return \Fuzz\MagicBox\Contracts\Repository
510 | */
511 | public function removeManyFillable(array $fillable): Repository
512 | {
513 | foreach ($fillable as $disallowed_field) {
514 | $this->removeFillable($disallowed_field);
515 | }
516 |
517 | return $this;
518 | }
519 |
520 | /**
521 | * Determine whether a given key is fillable
522 | *
523 | * @param string $key
524 | *
525 | * @return bool
526 | */
527 | public function isFillable(string $key): bool
528 | {
529 | if ($this->fillable === self::ALLOW_ALL) {
530 | return true;
531 | }
532 |
533 | return isset($this->fillable[$key]) && $this->fillable[$key];
534 | }
535 |
536 | /**
537 | * Set the relationships which can be included by the model
538 | *
539 | * @param array $includable
540 | *
541 | * @return \Fuzz\MagicBox\Contracts\Repository
542 | */
543 | public function setIncludable(array $includable): Repository
544 | {
545 | if ($includable === self::ALLOW_ALL) {
546 | $this->includable = self::ALLOW_ALL;
547 |
548 | return $this;
549 | }
550 |
551 | // Reset includable
552 | $this->includable = [];
553 |
554 | foreach ($includable as $allowed_include) {
555 | $this->includable[$allowed_include] = true;
556 | }
557 |
558 | return $this;
559 | }
560 |
561 | /**
562 | * Get the includable relationships
563 | *
564 | * @param bool $assoc
565 | *
566 | * @return array
567 | */
568 | public function getIncludable(bool $assoc = false): array
569 | {
570 | if ($this->includable === self::ALLOW_ALL) {
571 | return self::ALLOW_ALL;
572 | }
573 |
574 | return $assoc ? $this->includable : array_keys($this->includable);
575 | }
576 |
577 | /**
578 | * Add an includable relationship
579 | *
580 | * @param string $includable
581 | *
582 | * @return \Fuzz\MagicBox\Contracts\Repository
583 | */
584 | public function addIncludable(string $includable): Repository
585 | {
586 | $this->includable[$includable] = true;
587 |
588 | return $this;
589 | }
590 |
591 | /**
592 | * Add many includable fields
593 | *
594 | * @param array $includable
595 | *
596 | * @return \Fuzz\MagicBox\Contracts\Repository
597 | */
598 | public function addManyIncludable(array $includable): Repository
599 | {
600 | foreach ($includable as $allowed_include) {
601 | $this->addIncludable($allowed_include);
602 | }
603 |
604 | return $this;
605 | }
606 |
607 | /**
608 | * Remove an includable relationship
609 | *
610 | * @param string $includable
611 | *
612 | * @return \Fuzz\MagicBox\Contracts\Repository
613 | */
614 | public function removeIncludable(string $includable): Repository
615 | {
616 | unset($this->includable[$includable]);
617 |
618 | return $this;
619 | }
620 |
621 | /**
622 | * Remove many includable relationships
623 | *
624 | * @param array $includable
625 | *
626 | * @return \Fuzz\MagicBox\Contracts\Repository
627 | */
628 | public function removeManyIncludable(array $includable): Repository
629 | {
630 | foreach ($includable as $disallowed_include) {
631 | $this->removeIncludable($disallowed_include);
632 | }
633 |
634 | return $this;
635 | }
636 |
637 | /**
638 | * Determine whether a given key is includable
639 | *
640 | * @param string $key
641 | *
642 | * @return bool
643 | */
644 | public function isIncludable(string $key): bool
645 | {
646 | if ($this->includable === self::ALLOW_ALL) {
647 | return true;
648 | }
649 |
650 | return isset($this->includable[$key]) && $this->includable[$key];
651 | }
652 |
653 | /**
654 | * Set the fields which can be filtered on the model
655 | *
656 | * @param array $filterable
657 | *
658 | * @return \Fuzz\MagicBox\Contracts\Repository
659 | */
660 | public function setFilterable(array $filterable): Repository
661 | {
662 | if ($filterable === self::ALLOW_ALL) {
663 | $this->filterable = self::ALLOW_ALL;
664 |
665 | return $this;
666 | }
667 |
668 | // Reset filterable
669 | $this->filterable = [];
670 |
671 | foreach ($filterable as $allowed_field) {
672 | $this->filterable[$allowed_field] = true;
673 | }
674 |
675 | return $this;
676 | }
677 |
678 | /**
679 | * Get the filterable fields
680 | *
681 | * @param bool $assoc
682 | *
683 | * @return array
684 | */
685 | public function getFilterable(bool $assoc = false): array
686 | {
687 | if ($this->filterable === self::ALLOW_ALL) {
688 | return self::ALLOW_ALL;
689 | }
690 |
691 | return $assoc ? $this->filterable : array_keys($this->filterable);
692 | }
693 |
694 | /**
695 | * Add a filterable field
696 | *
697 | * @param string $filterable
698 | *
699 | * @return \Fuzz\MagicBox\Contracts\Repository
700 | */
701 | public function addFilterable(string $filterable): Repository
702 | {
703 | $this->filterable[$filterable] = true;
704 |
705 | return $this;
706 | }
707 |
708 | /**
709 | * Add many filterable fields
710 | *
711 | * @param array $filterable
712 | *
713 | * @return \Fuzz\MagicBox\Contracts\Repository
714 | */
715 | public function addManyFilterable(array $filterable): Repository
716 | {
717 | foreach ($filterable as $allowed_field) {
718 | $this->addFilterable($allowed_field);
719 | }
720 |
721 | return $this;
722 | }
723 |
724 | /**
725 | * Remove a filterable field
726 | *
727 | * @param string $filterable
728 | *
729 | * @return \Fuzz\MagicBox\Contracts\Repository
730 | */
731 | public function removeFilterable(string $filterable): Repository
732 | {
733 | unset($this->filterable[$filterable]);
734 |
735 | return $this;
736 | }
737 |
738 | /**
739 | * Remove many filterable fields
740 | *
741 | * @param array $filterable
742 | *
743 | * @return \Fuzz\MagicBox\Contracts\Repository
744 | */
745 | public function removeManyFilterable(array $filterable): Repository
746 | {
747 | foreach ($filterable as $disallowed_field) {
748 | $this->removeFilterable($disallowed_field);
749 | }
750 |
751 | return $this;
752 | }
753 |
754 | /**
755 | * Determine whether a given key is filterable
756 | *
757 | * @param string $key
758 | *
759 | * @return bool
760 | */
761 | public function isFilterable(string $key): bool
762 | {
763 | if ($this->filterable === self::ALLOW_ALL) {
764 | return true;
765 | }
766 |
767 | return isset($this->filterable[$key]) && $this->filterable[$key];
768 | }
769 |
770 | /**
771 | * Return a model's fields.
772 | *
773 | * @param \Illuminate\Database\Eloquent\Model $instance
774 | * @return array
775 | */
776 | public static function getFields(Model $instance): array
777 | {
778 | return Schema::getColumnListing($instance->getTable());
779 | }
780 |
781 | /**
782 | * Base query for all behaviors within this repository.
783 | *
784 | * @return \Illuminate\Database\Eloquent\Builder
785 | */
786 | protected function query()
787 | {
788 | $query = forward_static_call(
789 | [
790 | $this->getModelClass(),
791 | 'query',
792 | ]
793 | );
794 |
795 | $this->modifyQuery($query);
796 |
797 | $eager_loads = $this->getEagerLoads();
798 |
799 | if ( !empty($eager_loads)) {
800 | $this->safeWith($query, $eager_loads);
801 | }
802 |
803 | if ( !empty($modifiers = $this->getModifiers())) {
804 | foreach ($modifiers as $modifier) {
805 | $modifier($query);
806 | }
807 | }
808 |
809 | return $query;
810 | }
811 |
812 | /**
813 | * Process filter and sort modifications on $query
814 | *
815 | * @param \Illuminate\Database\Eloquent\Builder $query
816 | * @return void
817 | */
818 | protected function modifyQuery($query)
819 | {
820 | // Only include filters which have been whitelisted in $this->filterable
821 | $filters = $this->getFilterable() === self::ALLOW_ALL ?
822 | $this->getFilters() :
823 | Filter::intersectAllowedFilters($this->getFilters(), $this->getFilterable(true));
824 | $sort_order_options = $this->getSortOrder();
825 | $group_by = $this->getGroupBy();
826 | $aggregate = $this->getAggregate();
827 |
828 | // Check if filters or sorts are requested
829 | $filters_exist = !empty($filters);
830 | $sorts_exist = !empty($sort_order_options);
831 | $group_exist = !empty($group_by);
832 | $aggregate_exist = !empty($aggregate);
833 |
834 | // No modifications to apply
835 | if ( !$filters_exist && !$sorts_exist && !$group_exist && !$aggregate_exist) {
836 | return;
837 | }
838 |
839 | // Make a mock instance so we can describe its columns
840 | $model_class = $this->getModelClass();
841 | $temp_instance = new $model_class;
842 | $columns = $this->getFields($temp_instance);
843 |
844 | if ($filters_exist) {
845 | // Apply depth restrictions to each filter
846 | foreach ($filters as $filter => $value) {
847 | // Filters deeper than the depth restriction + 1 are not allowed
848 | // Depth restriction is offset by 1 because filters terminate with a column
849 | // i.e. 'users.posts.title' => '=Great Post' but the depth we expect is 'users.posts'
850 | if (count(explode(self::GLUE, $filter)) > ($this->getDepthRestriction() + 1)) {
851 | // Unset the disallowed filter
852 | unset($filters[$filter]);
853 | }
854 | }
855 |
856 | Filter::applyQueryFilters($query, $filters, $columns, $temp_instance->getTable());
857 | }
858 |
859 | // Modify the query with a group by condition.
860 | if ($group_exist) {
861 | $group = explode(',', reset($group_by));
862 | $group = array_map('trim', $group);
863 | $valid_group = array_intersect($group, $columns);
864 |
865 | $query->groupBy($valid_group);
866 | }
867 |
868 | // Run an aggregate function. We will only run one, no matter how many were submitted.
869 | if ($aggregate_exist) {
870 | $allowed_aggregations = [
871 | 'count',
872 | 'min',
873 | 'max',
874 | 'sum',
875 | 'avg',
876 | ];
877 | $allowed_columns = $columns;
878 | $column = reset($aggregate);
879 | $function = strtolower(key($aggregate));
880 |
881 | if (in_array($function, $allowed_aggregations, true) && in_array($column, $allowed_columns, true)) {
882 | $query->addSelect(DB::raw($function . '(' . $column . ') as aggregate'));
883 |
884 | if ($group_exist) {
885 | $query->addSelect($valid_group);
886 | }
887 | }
888 | }
889 |
890 | if ($sorts_exist) {
891 | $this->sortQuery($query, $sort_order_options, $temp_instance, $columns);
892 | }
893 |
894 | unset($temp_instance);
895 | }
896 |
897 | /**
898 | * Apply a sort to a database query
899 | *
900 | * @param \Illuminate\Database\Eloquent\Builder $query
901 | * @param array $sort_order_options
902 | * @param \Illuminate\Database\Eloquent\Model $temp_instance
903 | * @param array $columns
904 | */
905 | protected function sortQuery(Builder $query, array $sort_order_options, Model $temp_instance, array $columns)
906 | {
907 | $allowed_directions = [
908 | 'ASC',
909 | 'DESC',
910 | ];
911 |
912 | foreach ($sort_order_options as $order_by => $direction) {
913 | if (in_array(strtoupper($direction), $allowed_directions)) {
914 | $split = explode(self::GLUE, $order_by);
915 |
916 | // Sorts deeper than the depth restriction + 1 are not allowed
917 | // Depth restriction is offset by 1 because sorts terminate with a column
918 | // i.e. 'users.posts.title' => 'asc' but the depth we expect is 'users.posts'
919 | if (count($split) > ($this->getDepthRestriction() + 1)) {
920 | // Unset the disallowed sort
921 | unset($sort_order_options[$order_by]);
922 | continue;
923 | }
924 |
925 | if (in_array($order_by, $columns)) {
926 | $query->orderBy($order_by, $direction);
927 | } else {
928 | // Pull out orderBy field
929 | $field = array_pop($split);
930 |
931 | // Select only the base table fields, don't select relation data. Desired relation data
932 | // should be explicitly included
933 | $base_table = $temp_instance->getTable();
934 | $query->selectRaw("$base_table.*");
935 |
936 | $this->applyNestedJoins($query, $split, $temp_instance, $field, $direction);
937 | }
938 | }
939 | }
940 | }
941 |
942 | /**
943 | * Apply a depth restriction to an exploded dot-nested string (eager load, filter, etc)
944 | *
945 | * @param array $array
946 | * @return array
947 | */
948 | protected function applyDepthRestriction(array $array, $offset = 0)
949 | {
950 | return array_slice($array, 0, $this->getDepthRestriction() + $offset);
951 | }
952 |
953 | /**
954 | * "Safe" version of with eager-loading.
955 | *
956 | * Checks if relations exist before loading them.
957 | *
958 | * @param \Illuminate\Database\Eloquent\Builder $query
959 | * @param string|array $relations
960 | */
961 | protected function safeWith(Builder $query, $relations)
962 | {
963 | if (is_string($relations)) {
964 | $relations = func_get_args();
965 | array_shift($relations);
966 | }
967 |
968 | // Loop through all relations to check for valid relationship signatures
969 | foreach ($relations as $name => $constraints) {
970 | // Constraints may be passed in either form:
971 | // 2 => 'relation.nested'
972 | // or
973 | // 'relation.nested' => function() { ... }
974 | $constraints_are_name = is_numeric($name);
975 | $relation_name = $constraints_are_name ? $constraints : $name;
976 |
977 | // If this relation is not includable, skip
978 | // We expect to see foo.nested.relation in includable if the 3 level nested relationship is includable
979 | if (! $this->isIncludable($relation_name)) {
980 | unset($relations[$name]);
981 | continue;
982 | }
983 |
984 | // Expand the dot-notation to see all relations
985 | $nested_relations = explode(self::GLUE, $relation_name);
986 | $model = $query->getModel();
987 |
988 | // Don't allow eager loads beyond the eager load depth
989 | $nested_relations = $this->applyDepthRestriction($nested_relations);
990 |
991 | // We want to apply the depth restricted relations to the original relations array
992 | $cleaned_relation = join(self::GLUE, $nested_relations);
993 | if ($cleaned_relation === '') {
994 | unset($relations[$name]);
995 | } elseif ($constraints_are_name) {
996 | $relations[$name] = $cleaned_relation;
997 | } else {
998 | $relations[$cleaned_relation] = $constraints;
999 | unset($relations[$name]);
1000 | }
1001 |
1002 | foreach ($nested_relations as $index => $relation) {
1003 |
1004 | if ($this->isRelation($model, $relation, get_class($model))) {
1005 | // Iterate through relations if they actually exist
1006 | $model = $model->$relation()->getRelated();
1007 | } elseif ($index > 0) {
1008 | // If we found any valid relations, pass them through
1009 | $safe_relation = implode(self::GLUE, array_slice($nested_relations, 0, $index));
1010 | if ($constraints_are_name) {
1011 | $relations[$name] = $safe_relation;
1012 | } else {
1013 | unset($relations[$name]);
1014 | $relations[$safe_relation] = $constraints;
1015 | }
1016 | } else {
1017 | // If we didn't, remove this relation specification
1018 | unset($relations[$name]);
1019 | break;
1020 | }
1021 | }
1022 | }
1023 |
1024 | $query->with($relations);
1025 | }
1026 |
1027 | /**
1028 | * Apply nested joins to allow nested sorting for select relationship combinations
1029 | *
1030 | * @param \Illuminate\Database\Eloquent\Builder $query
1031 | * @param array $relations
1032 | * @param \Illuminate\Database\Eloquent\Model $instance
1033 | * @param $field
1034 | * @param string $direction
1035 | * @return void
1036 | */
1037 | public function applyNestedJoins(Builder $query, array $relations, Model $instance, $field, $direction = 'asc')
1038 | {
1039 | $base_table = $instance->getTable();
1040 |
1041 | // The current working relation
1042 | $relation = $relations[0];
1043 |
1044 | // Current working table
1045 | $table = Str::plural($relation);
1046 | $singular = Str::singular($relation);
1047 | $class = get_class($instance);
1048 |
1049 | // If the relation exists, determine which type (singular, multiple)
1050 | if ($this->isRelation($instance, $singular, $class)) {
1051 | $related = $instance->$singular();
1052 | } elseif ($this->isRelation($instance, $relation, $class)) {
1053 | $related = $instance->$relation();
1054 | } else {
1055 | // This relation does not exist
1056 | return;
1057 | }
1058 |
1059 | $foreign_key = $related->getForeignKey();
1060 |
1061 | // Join tables differently depending on relationship type
1062 | switch (get_class($related)) {
1063 | case BelongsToMany::class:
1064 | /**
1065 | * @var \Illuminate\Database\Eloquent\Relations\BelongsToMany $related
1066 | */
1067 | $base_table_key = $instance->getKeyName();
1068 | $relation_primary_key = $related->getModel()->getKeyName();
1069 |
1070 | // Join through the pivot table
1071 | $query->join($related->getTable(), "$base_table.$base_table_key", '=', $foreign_key);
1072 | $query->join($table, $related->getOtherKey(), '=', "$relation.$relation_primary_key");
1073 | break;
1074 | case HasMany::class:
1075 | /**
1076 | * @var \Illuminate\Database\Eloquent\Relations\HasMany $related
1077 | */
1078 | $base_table_key = $instance->getKeyName();
1079 |
1080 | // Join child's table
1081 | $query->join($table, "$base_table.$base_table_key", '=', $foreign_key);
1082 | break;
1083 | case BelongsTo::class:
1084 | /**
1085 | * @var \Illuminate\Database\Eloquent\Relations\BelongsTo $related
1086 | */
1087 | $relation_key = $related->getOtherKey();
1088 |
1089 | // Join related's table on the base table's foreign key
1090 | $query->join($table, "$base_table.$foreign_key", '=', "$table.$relation_key");
1091 | break;
1092 | case HasOne::class:
1093 | /**
1094 | * @var \Illuminate\Database\Eloquent\Relations\HasOne $related
1095 | */
1096 | $parent_key = $instance->getKeyName();
1097 |
1098 | // Join related's table on the base table's foreign key
1099 | $query->join($table, "$base_table.$parent_key", '=', "$foreign_key");
1100 | break;
1101 | }
1102 |
1103 | // @todo is it necessary to allow nested relationships further than the first/second degrees?
1104 | array_shift($relations);
1105 |
1106 | if (count($relations) >= 1) {
1107 | $this->applyNestedJoins($query, $relations, $related->getModel(), $field, $direction);
1108 | } else {
1109 | $query->orderBy("$table.$field", $direction);
1110 | }
1111 | }
1112 |
1113 | /**
1114 | * Find an instance of a model by ID.
1115 | *
1116 | * @param int $id
1117 | * @return \Illuminate\Database\Eloquent\Model
1118 | */
1119 | public function find($id)
1120 | {
1121 | return $this->query()->find($id);
1122 | }
1123 |
1124 | /**
1125 | * Find an instance of a model by ID, or fail.
1126 | *
1127 | * @param int $id
1128 | * @return \Illuminate\Database\Eloquent\Model
1129 | */
1130 | public function findOrFail($id): Model
1131 | {
1132 | return $this->query()->findOrFail($id);
1133 | }
1134 |
1135 | /**
1136 | * Get all elements against the base query.
1137 | *
1138 | * @return \Illuminate\Database\Eloquent\Collection
1139 | */
1140 | public function all(): Collection
1141 | {
1142 | return $this->query()->get();
1143 | }
1144 |
1145 | /**
1146 | * Return paginated response.
1147 | *
1148 | * @param int $per_page
1149 | * @return \Illuminate\Contracts\Pagination\Paginator
1150 | */
1151 | public function paginate($per_page): Paginator
1152 | {
1153 | return $this->query()->paginate($per_page);
1154 | }
1155 |
1156 | /**
1157 | * Count all elements against the base query.
1158 | *
1159 | * @return int
1160 | */
1161 | public function count(): int
1162 | {
1163 | return $this->query()->count();
1164 | }
1165 |
1166 | /**
1167 | * Determine if the base query returns a nonzero count.
1168 | *
1169 | * @return bool
1170 | */
1171 | public function hasAny(): bool
1172 | {
1173 | return $this->count() > 0;
1174 | }
1175 |
1176 | /**
1177 | * Get a random value.
1178 | *
1179 | * @return \Illuminate\Database\Eloquent\Model
1180 | */
1181 | public function random(): Model
1182 | {
1183 | return $this->query()->orderByRaw('RAND()')->first();
1184 | }
1185 |
1186 | /**
1187 | * Get the primary key from input.
1188 | *
1189 | * @return mixed
1190 | */
1191 | public function getInputId()
1192 | {
1193 | $input = $this->getInput();
1194 |
1195 | /** @var Model $model */
1196 | $model = $this->getModelClass();
1197 |
1198 | // If the model or the input is not set, then we cannot get an id.
1199 | if (! $model || ! $input) {
1200 | return null;
1201 | }
1202 |
1203 | return array_get($input, (new $model)->getKeyName());
1204 | }
1205 |
1206 | /**
1207 | * Fill an instance of a model with all known fields.
1208 | *
1209 | * @param \Illuminate\Database\Eloquent\Model $instance
1210 | * @return mixed
1211 | * @todo support more relationship types, such as polymorphic ones!
1212 | */
1213 | protected function fill(Model $instance): bool
1214 | {
1215 | $input = $this->getInput();
1216 | $model_fields = $this->getFields($instance);
1217 | $before_relations = [];
1218 | $after_relations = [];
1219 | $instance_model = get_class($instance);
1220 | $safe_instance = new $instance_model;
1221 |
1222 | $input = ($safe_instance->getIncrementing()) ? array_except($input, [$instance->getKeyName()]) : $input;
1223 |
1224 | foreach ($input as $key => $value) {
1225 | if (($relation = $this->isRelation($instance, $key, $instance_model)) && $this->isFillable($key)) {
1226 | $relation_type = get_class($relation);
1227 |
1228 | switch ($relation_type) {
1229 | case BelongsTo::class:
1230 | $before_relations[] = [
1231 | 'relation' => $relation,
1232 | 'value' => $value,
1233 | ];
1234 | break;
1235 | case HasOne::class:
1236 | case HasMany::class:
1237 | case BelongsToMany::class:
1238 | $after_relations[] = [
1239 | 'relation' => $relation,
1240 | 'value' => $value,
1241 | ];
1242 | break;
1243 | }
1244 | } elseif ((in_array($key, $model_fields) || $instance->hasSetMutator($key)) && $this->isFillable($key)) {
1245 | $instance->{$key} = $value;
1246 | }
1247 | }
1248 |
1249 | unset($safe_instance);
1250 |
1251 | $this->applyRelations($before_relations, $instance);
1252 | $instance->save();
1253 | $this->applyRelations($after_relations, $instance);
1254 |
1255 | return true;
1256 | }
1257 |
1258 | /**
1259 | * Apply relations from an array to an instance model.
1260 | *
1261 | * @param array $specs
1262 | * @param \Illuminate\Database\Eloquent\Model $instance
1263 | * @return void
1264 | */
1265 | protected function applyRelations(array $specs, Model $instance)
1266 | {
1267 | foreach ($specs as $spec) {
1268 | $this->cascadeRelation($spec['relation'], $spec['value'], $instance);
1269 | }
1270 | }
1271 |
1272 | /**
1273 | * Cascade relations through saves on a model.
1274 | *
1275 | * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
1276 | * @param array $input
1277 | * @param \Illuminate\Database\Eloquent\Model $parent
1278 | *
1279 | * @return void
1280 | */
1281 | protected function cascadeRelation(Relation $relation, array $input, Model $parent = null)
1282 | {
1283 | // Make a child repository for containing the cascaded relationship through saves
1284 | $target_model_class = get_class($relation->getQuery()->getModel());
1285 | $relation_repository = (new self)->setModelClass($target_model_class);
1286 |
1287 | switch (get_class($relation)) {
1288 | case BelongsTo::class:
1289 | /**
1290 | * @var \Illuminate\Database\Eloquent\Relations\BelongsTo $relation
1291 | */
1292 | // For BelongsTo, simply associate by foreign key.
1293 | // (We don't have to assume the parent model exists to do this.)
1294 | $related = $relation_repository->setInput($input)->save();
1295 | $relation->associate($related);
1296 | break;
1297 | case HasMany::class:
1298 | /**
1299 | * @var \Illuminate\Database\Eloquent\Relations\HasMany $relation
1300 | */
1301 | // The parent model "owns" child models; any not specified here should be deleted.
1302 | $current_ids = $relation->pluck($this->getKeyName())->toArray();
1303 | $new_ids = array_filter(array_column($input, $this->getKeyName()));
1304 | $removed_ids = array_diff($current_ids, $new_ids);
1305 | if ( !empty($removed_ids)) {
1306 | $relation->whereIn($this->getKeyName(), $removed_ids)->delete();
1307 | }
1308 |
1309 | // Set foreign keys on the children from the parent, and save.
1310 | foreach ($input as $sub_input) {
1311 | $sub_input[$this->getRelationsForeignKeyName($relation)] = $parent->{$this->getKeyName()};
1312 | $relation_repository->setInput($sub_input)->save();
1313 | }
1314 | break;
1315 | case HasOne::class:
1316 | /**
1317 | * @var \Illuminate\Database\Eloquent\Relations\HasOne $relation
1318 | */
1319 | // The parent model "owns" the child model; if we have a new and/or different
1320 | // existing child model, delete the old one.
1321 | $current = $relation->getResults();
1322 | if ( !is_null($current)
1323 | && ( !isset($input[$this->getKeyName()]) || $current->{$this->getKeyName()} !== intval($input[$this->getKeyName()]))
1324 | ) {
1325 | $relation->delete();
1326 | }
1327 |
1328 | // Set foreign key on the child from the parent, and save.
1329 | $input[$this->getRelationsForeignKeyName($relation)] = $parent->{$this->getKeyName()};
1330 | $relation_repository->setInput($input)->save();
1331 | break;
1332 | case BelongsToMany::class:
1333 | /**
1334 | * @var \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation
1335 | */
1336 | // Find all the IDs to sync.
1337 | $ids = [];
1338 |
1339 | foreach ($input as $sub_input) {
1340 | $id = $relation_repository->setInput($sub_input)->save()->{$this->getKeyName()};
1341 |
1342 | // If we were passed pivot data, pass it through accordingly.
1343 | if (isset($sub_input['pivot'])) {
1344 | $ids[$id] = (array)$sub_input['pivot'];
1345 | } else {
1346 | $ids[] = $id;
1347 | }
1348 | }
1349 |
1350 | // Sync to save pivot table and optional extra data.
1351 | $relation->sync($ids);
1352 | break;
1353 | }
1354 | }
1355 |
1356 | /**
1357 | * Create a model.
1358 | *
1359 | * @return Model | Collection
1360 | */
1361 | public function create(): Model
1362 | {
1363 | $model_class = $this->getModelClass();
1364 | $instance = new $model_class;
1365 | $this->fill($instance);
1366 |
1367 | return $instance;
1368 | }
1369 |
1370 | /**
1371 | * Create many models.
1372 | *
1373 | * @return Collection
1374 | */
1375 | public function createMany(): Collection
1376 | {
1377 | $collection = new Collection();
1378 |
1379 | foreach ($this->getInput() as $item) {
1380 | $repository = clone $this;
1381 | $repository->setInput($item);
1382 | $collection->add($repository->create());
1383 | }
1384 |
1385 | return $collection;
1386 | }
1387 |
1388 | /**
1389 | * Read a model.
1390 | *
1391 | * @param int|string|null $id
1392 | *
1393 | * @return \Illuminate\Database\Eloquent\Model
1394 | */
1395 | public function read($id = null): Model
1396 | {
1397 | return $this->findOrFail($id ?? $this->getInputId());
1398 | }
1399 |
1400 | /**
1401 | * Update a model.
1402 | *
1403 | * @param int|string|null $id
1404 | *
1405 | * @return Model|Collection
1406 | */
1407 | public function update($id = null): Model
1408 | {
1409 | $instance = $this->read($id);
1410 | $this->fill($instance);
1411 |
1412 | return $this->read($instance->getKey());
1413 | }
1414 |
1415 | /**
1416 | * Updates many models.
1417 | *
1418 | * @return Collection
1419 | */
1420 | public function updateMany(): Collection
1421 | {
1422 | $collection = new Collection();
1423 |
1424 | foreach ($this->getInput() as $item) {
1425 | $repository = clone $this;
1426 | $repository->setInput($item);
1427 | $collection->add($repository->update($repository->getInputId()));
1428 | }
1429 |
1430 | return $collection;
1431 | }
1432 |
1433 | /**
1434 | * Delete a model.
1435 | *
1436 | * @param int|string|null $id
1437 | *
1438 | * @return bool
1439 | *
1440 | * @throws \Exception
1441 | */
1442 | public function delete($id = null): bool
1443 | {
1444 | $instance = $this->read($id);
1445 |
1446 | return $instance->delete();
1447 | }
1448 |
1449 | /**
1450 | * Save a model, regardless of whether or not it is "new".
1451 | *
1452 | * @param int|string|null $id
1453 | *
1454 | * @return Model|Collection
1455 | */
1456 | public function save($id = null): Model
1457 | {
1458 | $id = $id ?? $this->getInputId();
1459 |
1460 | if ($id) {
1461 | return $this->update($id);
1462 | }
1463 |
1464 | return $this->create();
1465 | }
1466 |
1467 | /**
1468 | * Checks if the input has many items.
1469 | *
1470 | * @return bool
1471 | */
1472 | public function isManyOperation(): bool
1473 | {
1474 | return ($this->getInput() && array_keys($this->getInput()) === range(0, count($this->getInput()) - 1));
1475 | }
1476 |
1477 | /**
1478 | * A helper method for backwards compatibility.
1479 | *
1480 | * In laravel 5.4 they renamed the method `getPlainForeignKey` to `getForeignKeyName`
1481 | *
1482 | * @param HasOneOrMany $relation
1483 | *
1484 | * @return string
1485 | */
1486 | private function getRelationsForeignKeyName(HasOneOrMany $relation): string
1487 | {
1488 | return method_exists($relation, 'getForeignKeyName') ? $relation->getForeignKeyName() : $relation->getPlainForeignKey();
1489 | }
1490 | }
--------------------------------------------------------------------------------
/tests/EloquentRepositoryTest.php:
--------------------------------------------------------------------------------
1 | setModelClass($model_class)->setDepthRestriction(3)->setInput($input);
30 | }
31 |
32 | return new EloquentRepository;
33 | }
34 |
35 | public function seedUsers()
36 | {
37 | $this->artisan->call('db:seed', [
38 | '--class' => FilterDataSeeder::class
39 | ]);
40 | }
41 |
42 | /**
43 | * @expectedException \InvalidArgumentException
44 | */
45 | public function testItRejectsUnfuzzyModels()
46 | {
47 | $repo = (new EloquentRepository)->setModelClass('NotVeryFuzzy');
48 | }
49 |
50 | public function testItCanCreateASimpleModel()
51 | {
52 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User')->save();
53 | $this->assertNotNull($user);
54 | $this->assertEquals($user->id, 1);
55 | }
56 |
57 | public function testItCanFindASimpleModel()
58 | {
59 | $repo = $this->getRepository('Fuzz\MagicBox\Tests\Models\User');
60 | $user = $repo->save();
61 | $found_user = $repo->find($user->id);
62 | $this->assertNotNull($found_user);
63 | $this->assertEquals($user->id, $found_user->id);
64 | }
65 |
66 | public function testItCountsCollections()
67 | {
68 | $repository = $this->getRepository('Fuzz\MagicBox\Tests\Models\User');
69 | $this->assertEquals($repository->count(), 0);
70 | $this->assertFalse($repository->hasAny());
71 | }
72 |
73 | public function testItPaginates()
74 | {
75 | $repository = $this->getRepository('Fuzz\MagicBox\Tests\Models\User');
76 | $first_user = $repository->setInput(['username' => 'bob'])->save();
77 | $second_user = $repository->setInput(['username' => 'sue'])->save();
78 |
79 | $paginator = $repository->paginate(1);
80 | $this->assertInstanceOf('Illuminate\Pagination\LengthAwarePaginator', $paginator);
81 | $this->assertTrue($paginator->hasMorePages());
82 | }
83 |
84 | public function testItEagerLoadsRelationsSafely()
85 | {
86 | $this->getRepository('Fuzz\MagicBox\Tests\Models\User', [
87 | 'username' => 'joe',
88 | 'posts' => [
89 | [
90 | 'title' => 'Some Great Post',
91 | ],
92 | ]
93 | ])->save();
94 |
95 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User')->setFilters(['username' => 'joe'])
96 | ->setEagerLoads([
97 | 'posts.nothing',
98 | 'nada'
99 | ])->all()->first();
100 |
101 | $this->assertNotNull($user);
102 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $user->posts);
103 | $this->assertInstanceOf('Fuzz\MagicBox\Tests\Models\Post', $user->posts->first());
104 | }
105 |
106 | public function testItCanFillModelFields()
107 | {
108 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', ['username' => 'bob'])->save();
109 | $this->assertNotNull($user);
110 | $this->assertEquals($user->username, 'bob');
111 | }
112 |
113 | public function testItUpdatesExistingModels()
114 | {
115 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', ['username' => 'bobby'])->save();
116 | $this->assertEquals($user->id, 1);
117 | $this->assertEquals($user->username, 'bobby');
118 |
119 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', [
120 | 'id' => 1,
121 | 'username' => 'sue'
122 | ])->save();
123 | $this->assertEquals($user->id, 1);
124 | $this->assertEquals($user->username, 'sue');
125 | }
126 |
127 | public function testItDeletesModels()
128 | {
129 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', ['username' => 'spammer'])->save();
130 | $this->assertEquals($user->id, 1);
131 | $this->assertTrue($user->exists());
132 |
133 | $this->getRepository('Fuzz\MagicBox\Tests\Models\User', ['id' => 1])->delete();
134 | $this->assertNull(User::find(1));
135 | }
136 |
137 | public function testItFillsBelongsToRelations()
138 | {
139 | $post = $this->getRepository('Fuzz\MagicBox\Tests\Models\Post', [
140 | 'title' => 'Some Great Post',
141 | 'user' => [
142 | 'username' => 'jimmy',
143 | ],
144 | ])->save();
145 |
146 | $this->assertNotNull($post->user);
147 | $this->assertEquals($post->user->username, 'jimmy');
148 | }
149 |
150 | public function testItFillsHasManyRelations()
151 | {
152 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', [
153 | 'username' => 'joe',
154 | 'posts' => [
155 | [
156 | 'title' => 'Some Great Post',
157 | ],
158 | [
159 | 'title' => 'Yet Another Great Post',
160 | ],
161 | ]
162 | ])->save();
163 |
164 | $this->assertEquals($user->posts->pluck('id')->toArray(), [
165 | 1,
166 | 2
167 | ]);
168 |
169 | $post = Post::find(2);
170 | $this->assertNotNull($post);
171 | $this->assertEquals($post->user_id, $user->id);
172 | $this->assertEquals($post->title, 'Yet Another Great Post');
173 |
174 | $this->getRepository('Fuzz\MagicBox\Tests\Models\User', [
175 | 'id' => $user->id,
176 | 'posts' => [
177 | [
178 | 'id' => 1,
179 | ],
180 | ],
181 | ])->save();
182 |
183 | $user->load('posts');
184 |
185 | $this->assertEquals($user->posts->pluck('id')->toArray(), [
186 | 1,
187 | ]);
188 |
189 | $post = Post::find(2);
190 | $this->assertNull($post);
191 | }
192 |
193 | public function testItFillsHasOneRelations()
194 | {
195 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', [
196 | 'username' => 'joe',
197 | 'profile' => [
198 | 'favorite_cheese' => 'brie',
199 | ],
200 | ])->save();
201 |
202 | $this->assertNotNull($user->profile);
203 | $this->assertEquals($user->profile->favorite_cheese, 'brie');
204 | $old_profile_id = $user->profile->id;
205 |
206 | $user = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', [
207 | 'id' => $user->id,
208 | 'profile' => [
209 | 'favorite_cheese' => 'pepper jack',
210 | ],
211 | ])->save();
212 |
213 | $this->assertNotNull($user->profile);
214 | $this->assertEquals($user->profile->favorite_cheese, 'pepper jack');
215 |
216 | $this->assertNotEquals($user->profile->id, $old_profile_id);
217 | $this->assertNull(Profile::find($old_profile_id));
218 | }
219 |
220 | public function testItCascadesThroughSupportedRelations()
221 | {
222 | $post = $this->getRepository('Fuzz\MagicBox\Tests\Models\Post', [
223 | 'title' => 'All the Tags',
224 | 'user' => [
225 | 'username' => 'simon',
226 | 'profile' => [
227 | 'favorite_cheese' => 'brie',
228 | ],
229 | ],
230 | 'tags' => [
231 | [
232 | 'label' => 'Important Stuff',
233 | ],
234 | [
235 | 'label' => 'Less Important Stuff',
236 | ],
237 | ],
238 | ])->save();
239 |
240 | $this->assertEquals($post->tags()->count(), 2);
241 | $this->assertNotNull($post->user->profile);
242 | $this->assertNotNull($post->user->profile->favorite_cheese, 'brie');
243 | }
244 |
245 | public function testItUpdatesBelongsToManyPivots()
246 | {
247 | $post = $this->getRepository('Fuzz\MagicBox\Tests\Models\Post', [
248 | 'title' => 'All the Tags',
249 | 'user' => [
250 | 'username' => 'josh',
251 | ],
252 | 'tags' => [
253 | [
254 | 'label' => 'Has Extra',
255 | 'pivot' => [
256 | 'extra' => 'Meowth'
257 | ],
258 | ],
259 | ],
260 | ])->save();
261 |
262 | $tag = $post->tags->first();
263 | $this->assertEquals($tag->pivot->extra, 'Meowth');
264 |
265 | $post = $this->getRepository('Fuzz\MagicBox\Tests\Models\Post', [
266 | 'id' => $post->id,
267 | 'tags' => [
268 | [
269 | 'id' => $tag->id,
270 | 'pivot' => [
271 | 'extra' => 'Pikachu',
272 | ],
273 | ],
274 | ],
275 | ])->save();
276 |
277 | $tag = $post->tags->first();
278 | $this->assertEquals($tag->pivot->extra, 'Pikachu');
279 | }
280 |
281 | public function testItSorts()
282 | {
283 | $repository = $this->getRepository('Fuzz\MagicBox\Tests\Models\User');
284 | $first_user = $repository->setInput([
285 | 'username' => 'Bobby'
286 | ])->save();
287 | $second_user = $repository->setInput([
288 | 'username' => 'Robby'
289 | ])->save();
290 | $this->assertEquals($repository->all()->count(), 2);
291 |
292 | $found_users = $repository->setSortOrder([
293 | 'id' => 'desc'
294 | ])->all();
295 | $this->assertEquals($found_users->count(), 2);
296 | $this->assertEquals($found_users->first()->id, 2);
297 | }
298 |
299 | public function testItSortsNested()
300 | {
301 | $repository = $this->getRepository('Fuzz\MagicBox\Tests\Models\User');
302 | $first_user = $repository->setInput([
303 | 'username' => 'Bobby',
304 | 'posts' => [
305 | [
306 | 'title' => 'First Post',
307 | 'tags' => [
308 | ['label' => 'Tag1']
309 | ]
310 | ]
311 | ]
312 | ])->save();
313 | $second_user = $repository->setInput([
314 | 'username' => 'Robby',
315 | 'posts' => [
316 | [
317 | 'title' => 'Zis is the final post alphabetically',
318 | 'tags' => [
319 | ['label' => 'Tag2']
320 | ]
321 | ]
322 | ]
323 | ])->save();
324 | $third_user = $repository->setInput([
325 | 'username' => 'Gobby',
326 | 'posts' => [
327 | [
328 | 'title' => 'Third Post',
329 | 'tags' => [
330 | ['label' => 'Tag3']
331 | ]
332 | ]
333 | ]
334 | ])->save();
335 | $this->assertEquals($repository->all()->count(), 3);
336 |
337 | $found_users = $repository->setSortOrder([
338 | 'posts.title' => 'desc'
339 | ])->all();
340 | $this->assertEquals($found_users->count(), 3);
341 | $this->assertEquals($found_users->first()->username, 'Robby');
342 | }
343 |
344 | public function testItModifiesQueries()
345 | {
346 | $repository = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', ['username' => 'Billy']);
347 | $repository->save();
348 | $this->assertEquals($repository->count(), 1);
349 | $repository->setModifiers([
350 | function (Builder $query) {
351 | $query->whereRaw(DB::raw('0 = 1'));
352 | }
353 | ]);
354 | $this->assertEquals($repository->count(), 0);
355 | }
356 |
357 | public function testItAddsModifier()
358 | {
359 | $repository = $this->getRepository('Fuzz\MagicBox\Tests\Models\User', ['username' => 'Billy']);
360 | $repository->save();
361 | $this->assertEquals($repository->count(), 1);
362 | $repository->addModifier(function (Builder $query) {
363 | $query->whereRaw(DB::raw('0 = 1'));
364 | });
365 |
366 | $this->assertSame(1, count($repository->getModifiers()));
367 | $this->assertEquals($repository->count(), 0);
368 | }
369 |
370 | public function testItCanFilterOnFields()
371 | {
372 | $this->seedUsers();
373 |
374 | // Test that the repository implements filters correctly
375 | $repository = $this->getRepository(User::class);
376 | $this->assertEquals($repository->all()->count(), 4);
377 |
378 | $found_users = $repository->setFilters(['username' => '=chewbaclava@galaxyfarfaraway.com'])->all();
379 | $this->assertEquals($found_users->count(), 1);
380 | $this->assertEquals($found_users->first()->username, 'chewbaclava@galaxyfarfaraway.com');
381 | }
382 |
383 | public function testItOnlyUpdatesFillableAttributesOnCreate()
384 | {
385 | $input = [
386 | 'username' => 'javacup@galaxyfarfaraway.com',
387 | 'name' => 'Jabba The Hutt',
388 | 'hands' => 10,
389 | 'times_captured' => 0,
390 | 'not_fillable' => 'should be null',
391 | 'occupation' => 'Being Gross',
392 | 'profile' => [
393 | 'favorite_cheese' => 'Cheddar',
394 | 'favorite_fruit' => 'Apples',
395 | 'is_human' => false
396 | ],
397 | ];
398 |
399 | $user = $this->getRepository(User::class, $input)->save();
400 | $this->assertNull($user->not_fillable);
401 | }
402 |
403 | public function testItOnlyUpdatesFillableAttributesOnUpdate()
404 | {
405 | $input = [
406 | 'username' => 'javacup@galaxyfarfaraway.com',
407 | 'name' => 'Jabba The Hutt',
408 | 'hands' => 10,
409 | 'times_captured' => 0,
410 | 'not_fillable' => 'should be null',
411 | 'occupation' => 'Being Gross',
412 | 'profile' => [
413 | 'favorite_cheese' => 'Cheddar',
414 | 'favorite_fruit' => 'Apples',
415 | 'is_human' => false
416 | ],
417 | ];
418 |
419 | $user = $this->getRepository(User::class, $input)->save();
420 | $this->assertNull($user->not_fillable);
421 |
422 | $input['id'] = $user->id;
423 | $user = $this->getRepository(User::class, $input)->update();
424 | $this->assertNull($user->not_fillable);
425 | }
426 |
427 | public function testItOnlyUpdatesFillableAttributesForRelationsOnCreate()
428 | {
429 | $input = [
430 | 'username' => 'javacup@galaxyfarfaraway.com',
431 | 'name' => 'Jabba The Hutt',
432 | 'hands' => 10,
433 | 'times_captured' => 0,
434 | 'not_fillable' => 'should be null',
435 | 'occupation' => 'Being Gross',
436 | 'profile' => [
437 | 'favorite_cheese' => 'Cheddar',
438 | 'favorite_fruit' => 'Apples',
439 | 'is_human' => false,
440 | 'not_fillable' => 'should be null'
441 | ],
442 | ];
443 |
444 | $user = $this->getRepository(User::class, $input)->save();
445 | $this->assertNull($user->not_fillable);
446 | $this->assertNull($user->profile->not_fillable);
447 | }
448 |
449 | public function testItOnlyUpdatesFillableAttributesForRelationsOnUpdate()
450 | {
451 | $input = [
452 | 'username' => 'javacup@galaxyfarfaraway.com',
453 | 'name' => 'Jabba The Hutt',
454 | 'hands' => 10,
455 | 'times_captured' => 0,
456 | 'not_fillable' => 'should be null',
457 | 'occupation' => 'Being Gross',
458 | 'profile' => [
459 | 'favorite_cheese' => 'Cheddar',
460 | 'favorite_fruit' => 'Apples',
461 | 'is_human' => false,
462 | 'not_fillable' => 'should be null'
463 | ],
464 | ];
465 |
466 | $user = $this->getRepository(User::class, $input)->save();
467 | $this->assertNull($user->not_fillable);
468 | $this->assertNull($user->profile->not_fillable);
469 |
470 | $input['id'] = $user->id;
471 | $user = $this->getRepository(User::class, $input)->update();
472 | $this->assertNull($user->not_fillable);
473 | $this->assertNull($user->profile->not_fillable);
474 | }
475 |
476 | public function testItDoesNotRunArbitraryMethodsOnActualInstance()
477 | {
478 | $input = [
479 | 'username' => 'javacup@galaxyfarfaraway.com',
480 | 'name' => 'Jabba The Hutt',
481 | 'hands' => 10,
482 | 'times_captured' => 0,
483 | 'not_fillable' => 'should be null',
484 | 'occupation' => 'Being Gross',
485 | ];
486 |
487 | $user = $this->getRepository(User::class, $input)->save();
488 | $this->assertNotNull($user);
489 |
490 | $input['delete'] = 'doesn\'t matter but this should not be run';
491 | $input['id'] = $user->id;
492 |
493 | // Since users are soft deletable, if this fails and we run a $user->delete(), magic box will delete the record
494 | // but then try to recreate it with the same ID and get a MySQL unique constraint error because the
495 | // original ID record exists but is soft deleted
496 | $user = $this->getRepository(User::class, $input)->update();
497 |
498 | $database_user = User::find($user->id);
499 |
500 | $this->assertNotNull($database_user);
501 | $this->assertNull($user->deleted_at);
502 | }
503 |
504 | public function testItCanSetDepthRestriction()
505 | {
506 | $input = [
507 | 'username' => 'javacup@galaxyfarfaraway.com',
508 | 'name' => 'Jabba The Hutt',
509 | 'hands' => 10,
510 | 'times_captured' => 0,
511 | 'not_fillable' => 'should be null',
512 | 'occupation' => 'Being Gross',
513 | ];
514 |
515 | $repository = $this->getRepository(User::class, $input);
516 | $this->assertEquals(3, $repository->getDepthRestriction()); // getRepository sets 3 by default
517 | $repository->setDepthRestriction(5);
518 | $this->assertEquals(5, $repository->getDepthRestriction());
519 | }
520 |
521 | public function testItDepthRestrictsEagerLoads()
522 | {
523 | $this->seedUsers();
524 |
525 | $users = $this->getRepository(User::class)
526 | ->setDepthRestriction(0)
527 | ->setEagerLoads(
528 | [
529 | 'posts.tags',
530 | ]
531 | )->all()->toArray(); // toArray so we don't pull relations
532 |
533 | foreach ($users as $user) {
534 | $this->assertTrue(!isset($user['posts']));
535 | $this->assertTrue(!isset($user['posts']['tags'])); // We should load neither
536 | }
537 |
538 | $users = $this->getRepository(User::class)
539 | ->setDepthRestriction(1)
540 | ->setEagerLoads(
541 | [
542 | 'posts',
543 | 'posts.tags',
544 | ]
545 | )->all()->toArray(); // toArray so we don't pull relations
546 |
547 | foreach ($users as $user) {
548 | $this->assertTrue(isset($user['posts']));
549 | $this->assertTrue(isset($user['posts'][0]));
550 | $this->assertTrue(!isset($user['posts'][0]['tags'])); // We should load posts (1 level) but not tags (2 levels)
551 | }
552 |
553 | $users = $this->getRepository(User::class)
554 | ->setDepthRestriction(2)
555 | ->setEagerLoads(
556 | [
557 | 'posts',
558 | 'posts.user',
559 | ]
560 | )->all()->toArray(); // toArray so we don't pull relations
561 |
562 | foreach ($users as $user) {
563 | $this->assertTrue(isset($user['posts']));
564 | $this->assertTrue(isset($user['posts'][0]));
565 | $this->assertTrue(isset($user['posts'][0]['user'])); // We should load both
566 | }
567 | }
568 |
569 | public function testItDepthRestrictsFilters()
570 | {
571 | $this->seedUsers();
572 |
573 | /**
574 | * Test with 0 depth, filter too long
575 | */
576 | $users = $this->getRepository(User::class)
577 | ->setDepthRestriction(0)
578 | ->setFilters(
579 | [
580 | 'posts.tags.label' => '=#mysonistheworst'
581 | ]
582 | )
583 | ->all();
584 |
585 | // Filter should not apply because depth restriction is 0
586 | $this->assertEquals(User::all()->count(), $users->count());
587 |
588 | /**
589 | * Test with 1 depth, filter is allowed
590 | */
591 | $users = $this->getRepository(User::class)
592 | ->setDepthRestriction(1)
593 | ->setFilters(
594 | [
595 | 'posts.title' => '~10 Easy Ways to Clean'
596 | ]
597 | )
598 | ->all();
599 |
600 | // Filter should apply because depth restriction is 1
601 | $this->assertEquals(1, $users->count());
602 | $this->assertEquals('solocup@galaxyfarfaraway.com', $users->first()->username);
603 |
604 | /**
605 | * Test with 1 depth, filter is too long
606 | */
607 | $users = $this->getRepository(User::class)
608 | ->setDepthRestriction(1)
609 | ->setFilters(
610 | [
611 | 'posts.tags.label' => '=#mysonistheworst'
612 | ]
613 | )
614 | ->all();
615 |
616 | // Filter should apply because depth restriction is 1
617 | $this->assertEquals(User::all()->count(), $users->count());
618 |
619 | /**
620 | * Test with 1 depth, filter is okay
621 | */
622 | $users = $this->getRepository(User::class)
623 | ->setDepthRestriction(2)
624 | ->setFilters(
625 | [
626 | 'posts.tags.label' => '=#mysonistheworst'
627 | ]
628 | )
629 | ->all();
630 |
631 | // Filter should not apply because depth restriction is 2
632 | $this->assertEquals(2, $users->count());
633 |
634 | foreach ($users as $user) {
635 | $this->assertTrue(in_array($user->username, ['solocup@galaxyfarfaraway.com', 'lorgana@galaxyfarfaraway.com']));
636 | }
637 | }
638 |
639 | public function testItCanSortQueryAscending()
640 | {
641 | $this->seedUsers();
642 |
643 | $users = $this->getRepository(User::class)
644 | ->setSortOrder(['times_captured' => 'asc'])
645 | ->all();
646 |
647 | $this->assertEquals(User::all()->count(), $users->count());
648 |
649 | $previous_user = null;
650 | foreach ($users as $index => $user) {
651 | if ($index > 0) {
652 | $this->assertTrue($user->times_captured > $previous_user->times_captured);
653 | }
654 |
655 | $previous_user = $user;
656 | }
657 | }
658 |
659 | public function testItCanSortQueryDescending()
660 | {
661 | $this->seedUsers();
662 |
663 | $users = $this->getRepository(User::class)
664 | ->setSortOrder(['times_captured' => 'desc'])
665 | ->all();
666 |
667 | $this->assertEquals(User::all()->count(), $users->count());
668 |
669 | $previous_user = null;
670 | foreach ($users as $index => $user) {
671 | if ($index > 0) {
672 | $this->assertTrue($user->times_captured < $previous_user->times_captured);
673 | }
674 |
675 | $previous_user = $user;
676 | }
677 | }
678 |
679 | public function testItDepthRestrictsSorts()
680 | {
681 | $this->seedUsers();
682 |
683 | /**
684 | * Sort depth zero, expect sorting by top level ID
685 | */
686 | $users = $this->getRepository(User::class)
687 | ->setDepthRestriction(0)
688 | ->setSortOrder(['profile.favorite_cheese' => 'asc'])
689 | ->all();
690 |
691 | $this->assertEquals(User::all()->count(), $users->count());
692 |
693 | $previous_user = null;
694 | foreach ($users as $index => $user) {
695 | if ($index > 0) {
696 | $this->assertTrue($user->id > $previous_user->id);
697 | }
698 |
699 | $previous_user = $user;
700 | }
701 |
702 | /**
703 | * Sort depth 1, expect sorting by favorite cheese, asc alphabetical
704 | */
705 | $users = $this->getRepository(User::class)
706 | ->setDepthRestriction(1)
707 | ->setSortOrder(['profile.favorite_cheese' => 'asc'])
708 | ->all();
709 |
710 | $this->assertEquals(User::all()->count(), $users->count());
711 |
712 | $previous_user = null;
713 | $order = [];
714 | foreach ($users as $index => $user) {
715 | $order[] = $user->username;
716 | if ($index > 0) {
717 | // String 1 (Gouda) should be greater than (comes later alphabetically) than string 2 (Cheddar)
718 | $this->assertTrue(strcmp($user->profile->favorite_cheese, $previous_user->profile->favorite_cheese) > 0);
719 | }
720 |
721 | $previous_user = $user;
722 | }
723 |
724 | /**
725 | * Sort depth 1, expect sorting by favorite cheese, desc alphabetical
726 | */
727 | $users = $this->getRepository(User::class)
728 | ->setDepthRestriction(1)
729 | ->setSortOrder(['profile.favorite_cheese' => 'desc'])
730 | ->all();
731 |
732 | $this->assertEquals(User::all()->count(), $users->count());
733 |
734 | $previous_user = null;
735 | foreach ($users as $index => $user) {
736 | if ($index > 0) {
737 | // String 1 (Cheddar) should be less than (comes before alphabetically) than string 2 (Gouda)
738 | $this->assertTrue(strcmp($user->profile->favorite_cheese, $previous_user->profile->favorite_cheese) < 0);
739 | }
740 |
741 | $previous_user = $user;
742 | }
743 | }
744 |
745 | public function testItCanSortBelongsToRelation()
746 | {
747 | $this->seedUsers();
748 | /**
749 | * Sort depth 1, expect sorting by favorite cheese, asc alphabetical
750 | */
751 | $profiles = $this->getRepository(Profile::class)
752 | ->setSortOrder(['users.username' => 'asc'])
753 | ->setEagerLoads(['user'])
754 | ->all()->toArray();
755 |
756 | $this->assertEquals(Profile::all()->count(), count($profiles));
757 |
758 | $previous_profile = null;
759 | $order = [];
760 | foreach ($profiles as $index => $profile) {
761 | $order[] = $profile['user']['username'];
762 | if ($index > 0) {
763 | // String 1 (Gouda) should be greater than (comes later alphabetically) than string 2 (Cheddar)
764 | $this->assertTrue(strcmp($profile['user']['username'], $previous_profile['user']['username']) > 0);
765 | }
766 |
767 | $previous_profile = $profile;
768 | }
769 | }
770 |
771 | public function testItCanSortBelongsToManyRelation()
772 | {
773 | $this->seedUsers();
774 |
775 | /**
776 | * Sort depth 1, expect sorting by favorite cheese, asc alphabetical
777 | */
778 | $tags = $this->getRepository(Tag::class)
779 | ->setSortOrder(['posts.title' => 'asc'])
780 | ->setEagerLoads(['posts'])
781 | ->all()->toArray();
782 |
783 | $this->assertEquals(Tag::all()->count(), count($tags));
784 |
785 | foreach ($tags as $index => $tag) {
786 | $previous_post = null;
787 | $order = [];
788 | foreach ($tag['posts'] as $post) {
789 | $order[] = $post['title'];
790 | if ($index > 0) {
791 | // String 1 (Gouda) should be greater than (comes later alphabetically) than string 2 (Cheddar)
792 | $this->assertTrue(strcmp($post['title'], $previous_post['title']) > 0);
793 | }
794 |
795 | $previous_post = $post;
796 | }
797 | }
798 | }
799 |
800 | public function testItCanAddMultipleAdditionalFilters()
801 | {
802 | $this->seedUsers();
803 |
804 | $repository = $this->getRepository(User::class);
805 | $this->assertEquals($repository->all()->count(), 4);
806 |
807 | $found_users = $repository->setFilters(['username' => '~galaxyfarfaraway.com'])->all();
808 | $this->assertEquals($found_users->count(), 4);
809 |
810 | $additional_filters = [
811 | 'profile.is_human' => '=true',
812 | 'times_captured' => '>2'
813 | ];
814 |
815 | $found_users = $repository->addFilters($additional_filters)->all();
816 | $this->assertEquals($found_users->count(), 2);
817 |
818 | $filters = $repository->getFilters();
819 | $this->assertEquals([
820 | 'username' => '~galaxyfarfaraway.com',
821 | 'profile.is_human' => '=true',
822 | 'times_captured' => '>2'
823 | ], $filters);
824 | }
825 |
826 | public function testItCanAddOneAdditionalFilter()
827 | {
828 | $this->seedUsers();
829 |
830 | $repository = $this->getRepository(User::class);
831 | $this->assertEquals($repository->all()->count(), 4);
832 |
833 | $found_users = $repository->setFilters(['username' => '~galaxyfarfaraway.com'])->all();
834 | $this->assertEquals($found_users->count(), 4);
835 |
836 | $found_users = $repository->addFilter('profile.is_human', '=true')->all();
837 | $this->assertEquals($found_users->count(), 3);
838 |
839 | $filters = $repository->getFilters();
840 | $this->assertEquals([
841 | 'username' => '~galaxyfarfaraway.com',
842 | 'profile.is_human' => '=true',
843 | ], $filters);
844 | }
845 |
846 | public function testItCanSetFillable()
847 | {
848 | $repository = $this->getRepository(User::class);
849 |
850 | $this->assertSame(User::FILLABLE, $repository->getFillable());
851 |
852 | $repository->setFillable(['foo']);
853 |
854 | $this->assertSame(['foo'], $repository->getFillable());
855 | }
856 |
857 | public function testItCanAddFillable()
858 | {
859 | $repository = $this->getRepository(User::class);
860 |
861 | $this->assertSame(User::FILLABLE, $repository->getFillable());
862 |
863 | $repository->addFillable('foo');
864 |
865 | $expect = User::FILLABLE;
866 | $expect[] = 'foo';
867 |
868 | $this->assertSame($expect, $repository->getFillable());
869 | }
870 |
871 | public function testItCanAddManyFillable()
872 | {
873 | $repository = $this->getRepository(User::class);
874 |
875 | $this->assertSame(User::FILLABLE, $repository->getFillable());
876 |
877 | $repository->addManyFillable(['foo', 'bar', 'baz']);
878 |
879 | $expect = User::FILLABLE;
880 | $expect[] = 'foo';
881 | $expect[] = 'bar';
882 | $expect[] = 'baz';
883 |
884 | $this->assertSame($expect, $repository->getFillable());
885 | }
886 |
887 | public function testItCanRemoveFillable()
888 | {
889 | $repository = $this->getRepository(User::class);
890 |
891 | $this->assertSame(User::FILLABLE, $repository->getFillable());
892 |
893 | $repository->setFillable([
894 | 'foo',
895 | 'baz',
896 | 'bag',
897 | ]);
898 |
899 | $this->assertSame([
900 | 'foo',
901 | 'baz',
902 | 'bag',
903 | ], $repository->getFillable());
904 |
905 | $repository->removeFillable('baz');
906 |
907 | $this->assertSame(['foo', 'bag'], $repository->getFillable());
908 | }
909 |
910 | public function testItCanRemoveManyFillable()
911 | {
912 | $repository = $this->getRepository(User::class);
913 |
914 | $this->assertSame(User::FILLABLE, $repository->getFillable());
915 |
916 | $repository->setFillable([
917 | 'foo',
918 | 'baz',
919 | 'bag',
920 | ]);
921 |
922 | $this->assertSame([
923 | 'foo',
924 | 'baz',
925 | 'bag',
926 | ], $repository->getFillable());
927 |
928 | $repository->removeManyFillable(['baz', 'bag']);
929 |
930 | $this->assertSame(['foo',], $repository->getFillable());
931 | }
932 |
933 | public function testItCanDetermineIfIsFillable()
934 | {
935 | $repository = $this->getRepository(User::class);
936 |
937 | $this->assertSame(User::FILLABLE, $repository->getFillable());
938 |
939 | $repository->setFillable([
940 | '*' // allow all
941 | ]);
942 |
943 | $this->assertSame(EloquentRepository::ALLOW_ALL, $repository->getFillable());
944 |
945 | $this->assertTrue($repository->isFillable('foobar'));
946 |
947 | $repository->setFillable([
948 | 'foo',
949 | 'baz',
950 | 'bag',
951 | ]);
952 |
953 | $this->assertFalse($repository->isFillable('foobar'));
954 | $this->assertTrue($repository->isFillable('foo'));
955 | }
956 |
957 | public function testItCanSetIncludable()
958 | {
959 | $repository = $this->getRepository(User::class);
960 |
961 | $this->assertSame(User::INCLUDABLE, $repository->getIncludable());
962 |
963 | $repository->setIncludable([
964 | 'foo',
965 | 'bar',
966 | 'baz'
967 | ]);
968 |
969 | $this->assertSame(['foo', 'bar', 'baz'], $repository->getIncludable());
970 | }
971 |
972 | public function testItCanAddIncludable()
973 | {
974 | $repository = $this->getRepository(User::class);
975 |
976 | $this->assertSame(User::INCLUDABLE, $repository->getIncludable());
977 |
978 | $repository->setIncludable([
979 | 'foo',
980 | 'bar',
981 | 'baz'
982 | ]);
983 |
984 | $repository->addIncludable('foobar');
985 |
986 | $this->assertSame(['foo', 'bar', 'baz', 'foobar'], $repository->getIncludable());
987 | }
988 |
989 | public function testItCanAddManyIncludable()
990 | {
991 | $repository = $this->getRepository(User::class);
992 |
993 | $this->assertSame(User::INCLUDABLE, $repository->getIncludable());
994 |
995 | $repository->setIncludable([
996 | 'foo',
997 | 'bar',
998 | 'baz'
999 | ]);
1000 |
1001 | $repository->addManyIncludable(['foobar', 'bazbat']);
1002 |
1003 | $this->assertSame(['foo', 'bar', 'baz', 'foobar', 'bazbat'], $repository->getIncludable());
1004 | }
1005 |
1006 | public function testItCanRemoveIncludable()
1007 | {
1008 | $repository = $this->getRepository(User::class);
1009 |
1010 | $this->assertSame(User::INCLUDABLE, $repository->getIncludable());
1011 |
1012 | $repository->setIncludable([
1013 | 'foo',
1014 | 'bar',
1015 | 'baz'
1016 | ]);
1017 |
1018 | $repository->removeIncludable('foo');
1019 |
1020 | $this->assertSame(['bar', 'baz'], $repository->getIncludable());
1021 | }
1022 |
1023 | public function testItCanRemoveManyIncludable()
1024 | {
1025 | $repository = $this->getRepository(User::class);
1026 |
1027 | $this->assertSame(User::INCLUDABLE, $repository->getIncludable());
1028 |
1029 | $repository->setIncludable([
1030 | 'foo',
1031 | 'bar',
1032 | 'baz'
1033 | ]);
1034 |
1035 | $repository->removeManyIncludable(['foo', 'bar']);
1036 |
1037 | $this->assertSame(['baz'], $repository->getIncludable());
1038 | }
1039 |
1040 | public function testItCanDetermineIsIncludable()
1041 | {
1042 | $repository = $this->getRepository(User::class);
1043 |
1044 | $this->assertSame(User::INCLUDABLE, $repository->getIncludable());
1045 |
1046 | $repository->setIncludable([
1047 | '*' // Allow all
1048 | ]);
1049 |
1050 | $this->assertSame(EloquentRepository::ALLOW_ALL, $repository->getIncludable());
1051 |
1052 | $this->assertTrue($repository->isIncludable('foobar'));
1053 |
1054 | $repository->setIncludable([
1055 | 'foo',
1056 | 'bar',
1057 | 'baz'
1058 | ]);
1059 |
1060 | $this->assertFalse($repository->isIncludable('foobar'));
1061 | $this->assertTrue($repository->isIncludable('foo'));
1062 | }
1063 |
1064 | public function testItCanSetFilterable()
1065 | {
1066 | $repository = $this->getRepository(User::class);
1067 |
1068 | $this->assertSame(User::FILTERABLE, $repository->getFilterable());
1069 |
1070 | $repository->setFilterable([
1071 | 'foo',
1072 | 'bar',
1073 | 'baz',
1074 | ]);
1075 |
1076 | $this->assertSame([
1077 | 'foo',
1078 | 'bar',
1079 | 'baz',
1080 | ], $repository->getFilterable());
1081 | }
1082 |
1083 | public function testItCanAddFilterable()
1084 | {
1085 | $repository = $this->getRepository(User::class);
1086 |
1087 | $this->assertSame(User::FILTERABLE, $repository->getFilterable());
1088 |
1089 | $repository->setFilterable([
1090 | 'foo',
1091 | 'bar',
1092 | 'baz',
1093 | ]);
1094 |
1095 | $repository->addFilterable('foobar');
1096 |
1097 | $this->assertSame([
1098 | 'foo',
1099 | 'bar',
1100 | 'baz',
1101 | 'foobar',
1102 | ], $repository->getFilterable());
1103 | }
1104 |
1105 | public function testItCanAddManyFilterable()
1106 | {
1107 | $repository = $this->getRepository(User::class);
1108 |
1109 | $this->assertSame(User::FILTERABLE, $repository->getFilterable());
1110 |
1111 | $repository->setFilterable([
1112 | 'foo',
1113 | 'bar',
1114 | 'baz',
1115 | ]);
1116 |
1117 | $repository->addManyFilterable(['foobar', 'bazbat']);
1118 |
1119 | $this->assertSame([
1120 | 'foo',
1121 | 'bar',
1122 | 'baz',
1123 | 'foobar',
1124 | 'bazbat',
1125 | ], $repository->getFilterable());
1126 | }
1127 |
1128 | public function testItCanRemoveFilterable()
1129 | {
1130 | $repository = $this->getRepository(User::class);
1131 |
1132 | $this->assertSame(User::FILTERABLE, $repository->getFilterable());
1133 |
1134 | $repository->setFilterable([
1135 | 'foo',
1136 | 'bar',
1137 | 'baz',
1138 | ]);
1139 |
1140 | $repository->removeFilterable('bar');
1141 |
1142 | $this->assertSame(['foo', 'baz',], $repository->getFilterable());
1143 | }
1144 |
1145 | public function testItCanRemoveManyFilterable()
1146 | {
1147 | $repository = $this->getRepository(User::class);
1148 |
1149 | $this->assertSame(User::FILTERABLE, $repository->getFilterable());
1150 |
1151 | $repository->setFilterable([
1152 | 'foo',
1153 | 'bar',
1154 | 'baz',
1155 | ]);
1156 |
1157 | $repository->removeManyFilterable(['foo', 'baz']);
1158 |
1159 | $this->assertSame(['bar',], $repository->getFilterable());
1160 | }
1161 |
1162 | public function testItCanDetermineIsFilterable()
1163 | {
1164 | $repository = $this->getRepository(User::class);
1165 |
1166 | $this->assertSame(User::FILTERABLE, $repository->getFilterable());
1167 |
1168 | $repository->setFilterable([
1169 | '*'
1170 | ]);
1171 |
1172 | $this->assertSame(EloquentRepository::ALLOW_ALL, $repository->getFilterable());
1173 |
1174 | $this->assertTrue($repository->isFilterable('foobar'));
1175 |
1176 | $repository->setFilterable([
1177 | 'foo',
1178 | 'bar',
1179 | 'baz',
1180 | ]);
1181 |
1182 | $this->assertFalse($repository->isFilterable('foobar'));
1183 | $this->assertTrue($repository->isFilterable('foo'));
1184 | }
1185 |
1186 | public function testItDoesNotFillFieldThatIsNotFillable()
1187 | {
1188 | $post = $this->getRepository(
1189 | Post::class, [
1190 | 'title' => 'All the Tags',
1191 | 'not_fillable' => 'should not be set',
1192 | 'user' => [
1193 | 'username' => 'simon',
1194 | 'not_fillable' => 'should not be set',
1195 | 'profile' => [
1196 | 'favorite_cheese' => 'brie',
1197 | ],
1198 | ],
1199 | 'tags' => [
1200 | [
1201 | 'label' => 'Important Stuff',
1202 | 'not_fillable' => 'should not be set',
1203 | ],
1204 | [
1205 | 'label' => 'Less Important Stuff',
1206 | 'not_fillable' => 'should not be set',
1207 | ],
1208 | ],
1209 | ]
1210 | )->save();
1211 |
1212 | $this->assertEquals($post->tags()->count(), 2);
1213 | $this->assertNotNull($post->user->profile);
1214 | $this->assertNotNull($post->user->profile->favorite_cheese, 'brie');
1215 |
1216 | $this->assertNull($post->not_fillable);
1217 | $this->assertNull($post->user->not_fillable);
1218 | $this->assertNull($post->tags->get(0)->not_fillable);
1219 | $this->assertNull($post->tags->get(1)->not_fillable);
1220 | }
1221 |
1222 | public function testItDoesNotIncludeRelationThatIsNotIncludable()
1223 | {
1224 | $this->getRepository(
1225 | User::class, [
1226 | 'username' => 'joe',
1227 | 'posts' => [
1228 | [
1229 | 'title' => 'Some Great Post',
1230 | ],
1231 | ]
1232 | ]
1233 | )->save();
1234 |
1235 | $user = $this->getRepository(User::class)->setFilters(['username' => 'joe'])
1236 | ->setEagerLoads(
1237 | [
1238 | 'posts.nothing',
1239 | 'not_exists',
1240 | 'not_includable'
1241 | ]
1242 | )->all()->first();
1243 |
1244 | $this->assertNotNull($user);
1245 | $this->assertInstanceOf(Collection::class, $user->posts);
1246 | $this->assertInstanceOf(Post::class, $user->posts->first());
1247 |
1248 | $user = $user->toArray();
1249 |
1250 | $this->assertTrue(! isset($user['posts'][0]['nothing']));
1251 | $this->assertTrue(! isset($user['not_exists']));
1252 | $this->assertTrue(! isset($user['not_includable']));
1253 | }
1254 |
1255 | public function testItDoesNotFilterOnWhatIsNotFilterable()
1256 | {
1257 | $this->seedUsers();
1258 |
1259 | // Test that the repository implements filters correctly
1260 | $repository = $this->getRepository(User::class);
1261 | $this->assertEquals($repository->all()->count(), 4);
1262 |
1263 | $found_users = $repository->setFilters([
1264 | 'not_filterable' => '=foo', // Should not be applied
1265 | 'posts.not_filterable' => '=foo', // Should not be applied
1266 | ])->all();
1267 | $this->assertEquals($found_users->count(), 4); // No filters applied, expect to get all 4 users
1268 | }
1269 |
1270 | public function testItFiltersWithAllFieldsIfAllowAllIsSet()
1271 | {
1272 | $this->seedUsers();
1273 |
1274 | $repository = $this->getRepository(User::class);
1275 | $this->assertEquals($repository->all()->count(), 4);
1276 |
1277 | // Filters not applied
1278 | $repository->setFilterable([]);
1279 | $found_users = $repository->setFilters([
1280 | 'profile.is_human' => '=true',
1281 | 'times_captured' => '>2'
1282 | ])->all();
1283 | $this->assertEquals($found_users->count(), 4);
1284 |
1285 | // Filters now applied
1286 | $repository->setFilterable(EloquentRepository::ALLOW_ALL);
1287 | $found_users = $repository->setFilters([
1288 | 'profile.is_human' => '=true',
1289 | 'times_captured' => '>2'
1290 | ])->all();
1291 | $this->assertEquals($found_users->count(), 2);
1292 | }
1293 |
1294 | public function testItCanGetIncludableAsAssoc()
1295 | {
1296 | $repository = $this->getRepository(User::class);
1297 |
1298 | $repository->setIncludable([
1299 | 'foo',
1300 | 'bar',
1301 | 'baz',
1302 | ]);
1303 |
1304 | $this->assertSame([
1305 | 'foo' => true,
1306 | 'bar' => true,
1307 | 'baz' => true,
1308 | ], $repository->getIncludable(true));
1309 | }
1310 |
1311 | public function testItCanGetFillableAsAssoc()
1312 | {
1313 | $repository = $this->getRepository(User::class);
1314 |
1315 | $repository->setFillable([
1316 | 'foo',
1317 | 'bar',
1318 | 'baz',
1319 | ]);
1320 |
1321 | $this->assertSame([
1322 | 'foo' => true,
1323 | 'bar' => true,
1324 | 'baz' => true,
1325 | ], $repository->getFillable(true));
1326 | }
1327 |
1328 | public function testItCanGetFilterableAsAssoc()
1329 | {
1330 | $repository = $this->getRepository(User::class);
1331 |
1332 | $repository->setFilterable([
1333 | 'foo',
1334 | 'bar',
1335 | 'baz',
1336 | ]);
1337 |
1338 | $this->assertSame([
1339 | 'foo' => true,
1340 | 'bar' => true,
1341 | 'baz' => true,
1342 | ], $repository->getFilterable(true));
1343 | }
1344 |
1345 | public function testItCanAggregateQueryCount()
1346 | {
1347 |
1348 | }
1349 |
1350 | public function testItCanAggregateQueryMin()
1351 | {
1352 |
1353 | }
1354 |
1355 | public function testItCanAggregateQueryMax()
1356 | {
1357 |
1358 | }
1359 |
1360 | public function testItCanAggregateQuerySum()
1361 | {
1362 |
1363 | }
1364 |
1365 | public function testItCanAggregateQueryAverage()
1366 | {
1367 |
1368 | }
1369 |
1370 | public function testItCanGroupQuery()
1371 | {
1372 |
1373 | }
1374 |
1375 | /**
1376 | * @test
1377 | *
1378 | * The repository can create many models given an array of items.
1379 | */
1380 | public function testItCanCreateMany()
1381 | {
1382 | $data = [
1383 | ['username' => 'sue'],
1384 | ['username' => 'dave'],
1385 | ];
1386 |
1387 | $users = $this->getRepository(User::class, $data)->createMany();
1388 |
1389 | $this->assertInstanceOf(Collection::class, $users);
1390 | $this->assertEquals($users->where('username', '=', 'sue')->first()->username, 'sue');
1391 | $this->assertEquals($users->where('username', '=', 'dave')->first()->username, 'dave');
1392 | }
1393 |
1394 | /**
1395 | * @test
1396 | *
1397 | * The repository can update many models given an array of items with an id.
1398 | */
1399 | public function testItCanUpdateMany()
1400 | {
1401 | $userOne = $this->getRepository(User::class, ['username' => 'bobby'])->save();
1402 | $userTwo = $this->getRepository(User::class, ['username' => 'sam'])->save();
1403 | $this->assertEquals($userOne->getKey(), 1);
1404 | $this->assertEquals($userOne->username, 'bobby');
1405 | $this->assertEquals($userTwo->getKey(), 2);
1406 | $this->assertEquals($userTwo->username, 'sam');
1407 |
1408 | $users = $this->getRepository(User::class,[
1409 | ['id' => 1, 'username' => 'sue'],
1410 | ['id' => 2, 'username' => 'dave'],
1411 | ])->updateMany();
1412 |
1413 | $this->assertInstanceOf(Collection::class, $users);
1414 | $this->assertEquals($users->find(1)->id, 1);
1415 | $this->assertEquals($users->find(1)->username, 'sue');
1416 | $this->assertEquals($users->find(2)->id, 2);
1417 | $this->assertEquals($users->find(2)->username, 'dave');
1418 | }
1419 |
1420 | /**
1421 | * @test
1422 | *
1423 | * The repository can check if the input should be a many operation or not.
1424 | */
1425 | public function testItCanCheckIfManyOperation()
1426 | {
1427 | $notManyOperationData = ['id' => 1, 'username' => 'bobby'];
1428 | $manyOperationData = [['id' => 1, 'username' => 'bobby'], ['id' => 2, 'username' => 'sam']];
1429 |
1430 | $this->assertFalse($this->getRepository(User::class, [])->isManyOperation());
1431 | $this->assertFalse($this->getRepository(User::class, $notManyOperationData)->isManyOperation());
1432 | $this->assertTrue($this->getRepository(User::class, $manyOperationData)->isManyOperation());
1433 | }
1434 | }
1435 |
--------------------------------------------------------------------------------