diff --git a/composer.json b/composer.json
index 59701e78f..c531237c7 100644
--- a/composer.json
+++ b/composer.json
@@ -20,7 +20,8 @@
"require": {
"php": "^7.4",
"ext-json": "*",
- "illuminate/support": "^6.0|^7.0"
+ "illuminate/support": "^6.0|^7.0",
+ "doctrine/dbal": "^2.10"
},
"require-dev": {
"mockery/mockery": "^1.3",
diff --git a/config/config.php b/config/config.php
index 60c24a033..966bd7a06 100644
--- a/config/config.php
+++ b/config/config.php
@@ -41,7 +41,7 @@
|
*/
- 'base' => '/restify-api',
+ 'base' => '/api/restify',
/*
|--------------------------------------------------------------------------
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 000000000..ad975d9af
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ tests
+
+
+
+
+ src/
+
+ src/Commands
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/routes/api.php b/routes/api.php
index 1c4808627..2abec02b6 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -1,8 +1,14 @@
- */
class PolicyCommand extends GeneratorCommand
{
- /**
- * The console command name.
- *
- * @var string
- */
protected $name = 'restify:policy';
- /**
- * The console command description.
- *
- * @var string
- */
protected $description = 'Create a new policy for a specific model.';
- /**
- * The type of class being generated.
- *
- * @var string
- */
- protected $type = 'Policy';
-
- /**
- * Execute the console command.
- *
- * @return bool|null
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
- */
public function handle()
{
- parent::handle();
+ if (parent::handle() === false && ! $this->option('force')) {
+ return false;
+ }
}
- /**
- * Build the class with the given name.
- *
- * @param string $name
- * @return string
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
- */
protected function buildClass($name)
{
- $namespacedModel = null;
- $model = $this->option('model');
+ $class = $this->replaceModel(parent::buildClass($name));
- if (is_null($model)) {
- $model = $this->argument('name');
- }
+ $class = $this->replaceQualifiedModel($class);
- if ($model && ! Str::startsWith($model, [$this->laravel->getNamespace(), '\\'])) {
- $namespacedModel = $this->laravel->getNamespace().$model;
- }
+ return $class;
+ }
- $name .= 'Policy';
+ protected function replaceClass($stub, $name)
+ {
+ $class = str_replace($this->getNamespace($name).'\\', '', $this->guessPolicyName());
- $rendered = str_replace(
- 'UseDummyModel', $namespacedModel ?? $model, parent::buildClass($name)
- );
+ return str_replace(['{{ class }}', '{{class}}'], $class, $stub);
+ }
- $rendered = str_replace(
- 'DummyModel', $model, $rendered
- );
+ protected function replaceModel($stub)
+ {
+ return str_replace(['{{ model }}', '{{model}}'], class_basename($this->guessQualifiedModel()), $stub);
+ }
+
+ protected function replaceQualifiedModel($stub)
+ {
+ return str_replace('{{ modelQualified }}', $this->guessQualifiedModel(), $stub);
+ }
+
+ protected function guessQualifiedModel(): string
+ {
+ $model = Str::singular(class_basename(Str::beforeLast($this->getNameInput(), 'Policy')));
- return $rendered;
+ return str_replace('/', '\\', $this->rootNamespace().'Models/'.$model);
}
- public function nameWithEnd()
+ protected function guessPolicyName()
{
- $model = $this->option('model');
+ $name = $this->getNameInput();
- if (is_null($model)) {
- $model = $this->argument('name');
+ if (false === Str::endsWith($name, 'Policy')) {
+ $name .= 'Policy';
}
- return $model.'Policy';
+ return $name;
}
protected function getPath($name)
{
- return $this->laravel['path'].'/Policies/'.$this->nameWithEnd().'.php';
+ return $this->laravel['path'].'/Policies/'.$this->guessPolicyName().'.php';
}
/**
@@ -105,12 +81,12 @@ protected function getStub()
/**
* Get the default namespace for the class.
*
- * @param string $rootNamespace
+ * @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
- return $rootNamespace.'\Restify';
+ return $rootNamespace.'\Policies';
}
/**
@@ -122,6 +98,7 @@ protected function getOptions()
{
return [
['model', 'm', InputOption::VALUE_REQUIRED, 'The model class being protected.'],
+ ['force', null, InputOption::VALUE_NONE, 'Create the class even if the model already exists.'],
];
}
}
diff --git a/src/Commands/RepositoryCommand.php b/src/Commands/RepositoryCommand.php
index cc9229868..2612904e9 100644
--- a/src/Commands/RepositoryCommand.php
+++ b/src/Commands/RepositoryCommand.php
@@ -2,6 +2,7 @@
namespace Binaryk\LaravelRestify\Commands;
+use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption;
@@ -11,110 +12,161 @@
*/
class RepositoryCommand extends GeneratorCommand
{
- /**
- * The console command name.
- *
- * @var string
- */
+ use ConfirmableTrait;
+
protected $name = 'restify:repository';
- /**
- * The console command description.
- *
- * @var string
- */
protected $description = 'Create a new repository class';
- /**
- * The type of class being generated.
- *
- * @var string
- */
protected $type = 'Repository';
- /**
- * Execute the console command.
- *
- * @return bool|null
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
- */
public function handle()
{
- parent::handle();
+ if (parent::handle() === false && ! $this->option('force')) {
+ return false;
+ }
$this->callSilent('restify:base-repository', [
'name' => 'Repository',
]);
+
+ if ($this->option('all')) {
+ $this->input->setOption('factory', true);
+ $this->input->setOption('model', true);
+ $this->input->setOption('policy', true);
+ $this->input->setOption('table', true);
+ }
+
+ if ($this->option('policy')) {
+ $this->buildPolicy();
+ }
+
+ if ($this->option('model')) {
+ $this->buildModel();
+ }
+
+ if ($this->option('table')) {
+ $this->buildMigration();
+ }
+
+ if ($this->option('factory')) {
+ $this->buildFactory();
+ }
}
/**
* Build the class with the given name.
+ * This method should return the file class content.
*
- * @param string $name
+ * @param string $name
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function buildClass($name)
{
- $model = $this->option('model');
-
- if (is_null($model)) {
- $model = $this->laravel->getNamespace().$this->argument('name');
- } elseif (! Str::startsWith($model, [
- $this->laravel->getNamespace(), '\\',
- ])) {
- $model = $this->laravel->getNamespace().$model;
+ if (false === Str::endsWith($name, 'Repository')) {
+ $name .= 'Repository';
}
- if ($this->option('all')) {
- $this->call('make:model', [
- 'name' => $this->argument('name'),
- '--factory' => true,
- '--migration' => true,
- '--controller' => true,
- ]);
-
- $this->call('restify:policy', [
- 'name' => $this->argument('name'),
- ]);
+ return $this->replaceModel(parent::buildClass($name), $this->guessQualifiedModelName());
+ }
+
+ protected function replaceModel($stub, $class)
+ {
+ return str_replace(['DummyClass', '{{ model }}', '{{model}}'], $class, $stub);
+ }
+
+ protected function guessBaseModelClass()
+ {
+ return class_basename($this->guessQualifiedModelName());
+ }
+
+ protected function guessQualifiedModelName()
+ {
+ $model = Str::singular(class_basename(Str::before($this->getNameInput(), 'Repository')));
+
+ return str_replace('/', '\\', $this->rootNamespace().'/Models//'.$model);
+ }
+
+ protected function buildMigration()
+ {
+ $table = Str::snake(Str::pluralStudly(class_basename($this->guessQualifiedModelName())));
+
+ $guessMigration = 'Create'.Str::studly($table).'Table';
+
+ if (false === class_exists($guessMigration)) {
+ $migration = Str::snake($guessMigration);
+ $yes = $this->confirm("Do you want to generate the migration [{$migration}]?");
+
+ if ($yes) {
+ $this->call('make:migration', [
+ 'name' => $migration,
+ '--create' => $table,
+ ]);
+ }
}
+ }
+
+ protected function buildPolicy()
+ {
+ $this->call('restify:policy', [
+ 'name' => $this->guessBaseModelClass(),
+ ]);
- return str_replace(
- 'DummyFullModel', $model, parent::buildClass($name)
- );
+ return $this;
+ }
+
+ protected function buildModel()
+ {
+ $model = $this->guessQualifiedModelName();
+
+ if (false === class_exists($model)) {
+ $yes = $this->confirm("Do you want to generate the model [{$model}]?");
+
+ if ($yes) {
+ $this->call('make:model', ['name' => str_replace('\\\\', '\\', $model)]);
+ }
+ }
+ }
+
+ protected function buildFactory()
+ {
+ $factory = Str::studly(class_basename($this->guessQualifiedModelName()));
+
+ $this->call('make:factory', [
+ 'name' => "{$factory}Factory",
+ '--model' => str_replace('\\\\', '\\', $this->guessQualifiedModelName()),
+ ]);
}
- /**
- * Get the stub file for the generator.
- *
- * @return string
- */
protected function getStub()
{
return __DIR__.'/stubs/repository.stub';
}
- /**
- * Get the default namespace for the class.
- *
- * @param string $rootNamespace
- * @return string
- */
+ protected function getPath($name)
+ {
+ if (false === Str::endsWith($name, 'Repository')) {
+ $name .= 'Repository';
+ }
+
+ return parent::getPath($name);
+ }
+
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Restify';
}
- /**
- * Get the console command options.
- *
- * @return array
- */
protected function getOptions()
{
return [
['all', 'a', InputOption::VALUE_NONE, 'Generate a migration, factory, and controller for the repository'],
- ['model', 'm', InputOption::VALUE_REQUIRED, 'The model class being represented.'],
+ ['model', 'm', InputOption::VALUE_NONE, 'The model class being represented.'],
+ ['factory', 'f', InputOption::VALUE_NONE, 'Create a new factory for the repository model.'],
+ ['policy', 'p', InputOption::VALUE_NONE, 'Create a new policy for the repository model.'],
+ ['table', 't', InputOption::VALUE_NONE, 'Create a new migration table file for the repository model.'],
+ ['force', null, InputOption::VALUE_NONE, 'Create the class even if the model already exists.'],
];
}
}
diff --git a/src/Commands/SetupCommand.php b/src/Commands/SetupCommand.php
index b32b9bf4e..7546a3292 100644
--- a/src/Commands/SetupCommand.php
+++ b/src/Commands/SetupCommand.php
@@ -7,25 +7,10 @@
class SetupCommand extends Command
{
- /**
- * The name and signature of the console command.
- *
- * @var string
- */
protected $signature = 'restify:setup';
- /**
- * The console command description.
- *
- * @var string
- */
- protected $description = 'Prepare Restify dependencies and resources';
+ protected $description = 'Should be run when you firstly instal the package. It will setup everything for you.';
- /**
- * Execute the console command.
- *
- * @return void
- */
public function handle()
{
$this->comment('Publishing Restify Service Provider...');
@@ -40,7 +25,7 @@ public function handle()
$this->comment('Generating User Repository...');
$this->callSilent('restify:repository', ['name' => 'User']);
- copy(__DIR__.'/stubs/user-repository.stub', app_path('Restify/User.php'));
+ copy(__DIR__.'/stubs/user-repository.stub', app_path('Restify/UserRepository.php'));
$this->setAppNamespace();
diff --git a/src/Commands/StubCommand.php b/src/Commands/StubCommand.php
new file mode 100644
index 000000000..3d886b144
--- /dev/null
+++ b/src/Commands/StubCommand.php
@@ -0,0 +1,102 @@
+resolver = $resolver;
+ $this->faker = $faker;
+ }
+
+ public function handle()
+ {
+ if (! $this->confirmToProceed()) {
+ return true;
+ }
+
+ if (! $this->resolver->connection()->getSchemaBuilder()->hasTable($table = $this->argument('table'))) {
+ return false;
+ }
+
+ DB::connection()->getDoctrineSchemaManager()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
+
+ $start = microtime(true);
+ Collection::times($count = $this->option('count') ?? 1)->each(fn () => $this->make($table));
+
+ $time = round(microtime(true) - $start, 2);
+
+ $this->info("Seeded {$count} {$table} in {$time} seconds");
+ }
+
+ protected function make($table)
+ {
+ $data = [];
+
+ collect(Schema::getColumnListing($table))->each(function ($column) use (&$data, $table) {
+ $type = Schema::getColumnType($table, $column);
+
+ switch ($type) {
+ case 'string':
+ $data[$column] = $this->faker->text(50);
+
+ if (Str::contains($column, 'email')) {
+ $data[$column] = $this->faker->email;
+ }
+
+ if (Str::contains($column, 'password')) {
+ $data[$column] = Hash::make('secret');
+ }
+
+ if (Str::contains($column, 'uuid')) {
+ $data[$column] = Str::orderedUuid();
+ }
+
+ if (Str::contains($column, 'image') || Str::contains($column, 'picture')) {
+ $data[$column] = $this->faker->imageUrl();
+ }
+ break;
+ case 'datetime':
+ $data[$column] = Carbon::now();
+ break;
+ case 'boolean':
+ $data[$column] = $this->faker->boolean;
+ break;
+ case 'biging':
+ case 'int':
+ if (false === Str::endsWith($column, 'id')) {
+ $data[$column] = $this->faker->id;
+ }
+ break;
+ }
+ });
+
+ $id = DB::table($table)->insertGetId($data);
+
+ $this->info('Created '.Str::singular(Str::studly($table)).' with id:'.$id);
+ }
+}
diff --git a/src/Commands/stubs/base-repository.stub b/src/Commands/stubs/base-repository.stub
index ddd0cfc6c..34578e01b 100644
--- a/src/Commands/stubs/base-repository.stub
+++ b/src/Commands/stubs/base-repository.stub
@@ -8,30 +8,6 @@ use Illuminate\Contracts\Pagination\Paginator;
abstract class Repository extends RestifyRepository
{
- /**
- * Format the response for the details request (single item)
- *
- * @param $request
- * @param $serialized
- * @return array
- */
- public function serializeDetails($request, $serialized)
- {
- return $serialized;
- }
-
- /**
- * Format the response for the index request
- *
- * @param $request
- * @param $serialized
- * @return array
- */
- public function serializeIndex($request, $serialized)
- {
- return $serialized;
- }
-
/**
* Build an "index" query for the given repository.
*
diff --git a/src/Commands/stubs/policy.stub b/src/Commands/stubs/policy.stub
index 820efadda..43746bc56 100644
--- a/src/Commands/stubs/policy.stub
+++ b/src/Commands/stubs/policy.stub
@@ -4,30 +4,19 @@ namespace DummyNamespace;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
-use UseDummyModel;
+use {{ modelQualified }};
-class DummyClass
+class {{ class }}
{
use HandlesAuthorization;
/**
- * Determine whether the user can view any models.
- *
- * @param \App\User $user
- * @return mixed
- */
- public function showEvery(User $user = null)
- {
- //
- }
-
- /**
- * Determine whether the user is authorized to access the repository uriKey
- *
+ * Determine whether the user can use restify feature for each CRUD operation.
+ * So if this is not allowed, all operations will be disabled
* @param \App\User $user
* @return mixed
*/
- public function showAny(User $user = null)
+ public function allowRestify(User $user = null)
{
//
}
@@ -36,10 +25,10 @@ class DummyClass
* Determine whether the user can get the model.
*
* @param \App\User $user
- * @param DummyModel $model
+ * @param {{ model }} $model
* @return mixed
*/
- public function show(User $user, DummyModel $model)
+ public function show(User $user, {{ model }} $model)
{
//
}
@@ -59,10 +48,10 @@ class DummyClass
* Determine whether the user can update the model.
*
* @param \App\User $user
- * @param DummyModel $model
+ * @param {{ model }} $model
* @return mixed
*/
- public function update(User $user, DummyModel $model)
+ public function update(User $user, {{ model }} $model)
{
//
}
@@ -71,10 +60,10 @@ class DummyClass
* Determine whether the user can delete the model.
*
* @param \App\User $user
- * @param DummyModel $model
+ * @param {{ model }} $model
* @return mixed
*/
- public function delete(User $user, DummyModel $model)
+ public function delete(User $user, {{ model }} $model)
{
//
}
@@ -83,10 +72,10 @@ class DummyClass
* Determine whether the user can restore the model.
*
* @param \App\User $user
- * @param DummyModel $model
+ * @param {{ model }} $model
* @return mixed
*/
- public function restore(User $user, DummyModel $model)
+ public function restore(User $user, {{ model }} $model)
{
//
}
@@ -95,10 +84,10 @@ class DummyClass
* Determine whether the user can permanently delete the model.
*
* @param \App\User $user
- * @param DummyModel $model
+ * @param {{ model }} $model
* @return mixed
*/
- public function forceDelete(User $user, DummyModel $model)
+ public function forceDelete(User $user, {{ model }} $model)
{
//
}
diff --git a/src/Commands/stubs/repository.stub b/src/Commands/stubs/repository.stub
index bd4652929..15ece54e1 100644
--- a/src/Commands/stubs/repository.stub
+++ b/src/Commands/stubs/repository.stub
@@ -12,7 +12,7 @@ class DummyClass extends Repository
*
* @var string
*/
- public static $model = 'DummyFullModel';
+ public static $model = '{{ model }}';
/**
* @param RestifyRequest $request
diff --git a/src/Commands/stubs/user-repository.stub b/src/Commands/stubs/user-repository.stub
index 77f410e5e..04856d28f 100644
--- a/src/Commands/stubs/user-repository.stub
+++ b/src/Commands/stubs/user-repository.stub
@@ -6,7 +6,7 @@ use Binaryk\LaravelRestify\Fields\Field;
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
use Illuminate\Support\Facades\Hash;
-class User extends Repository
+class UserRepository extends Repository
{
/**
* The model the repository corresponds to.
diff --git a/src/Controllers/RestResponse.php b/src/Controllers/RestResponse.php
index 9f4905998..579039dd5 100644
--- a/src/Controllers/RestResponse.php
+++ b/src/Controllers/RestResponse.php
@@ -65,6 +65,24 @@ class RestResponse extends JsonResponse implements Responsable
const REST_RESPONSE_SUCCESS_CODE = 200;
const REST_RESPONSE_UNAVAILABLE_CODE = 503;
+ const CODES = [
+ self::REST_RESPONSE_AUTH_CODE,
+ self::REST_RESPONSE_REFRESH_CODE,
+ self::REST_RESPONSE_CREATED_CODE,
+ self::REST_RESPONSE_UPDATED_CODE,
+ self::REST_RESPONSE_DELETED_CODE,
+ self::REST_RESPONSE_BLANK_CODE,
+ self::REST_RESPONSE_ERROR_CODE,
+ self::REST_RESPONSE_INVALID_CODE,
+ self::REST_RESPONSE_UNAUTHORIZED_CODE,
+ self::REST_RESPONSE_FORBIDDEN_CODE,
+ self::REST_RESPONSE_MISSING_CODE,
+ self::REST_RESPONSE_NOTFOUND_CODE,
+ self::REST_RESPONSE_THROTTLE_CODE,
+ self::REST_RESPONSE_SUCCESS_CODE,
+ self::REST_RESPONSE_UNAVAILABLE_CODE,
+ ];
+
/**
* @var ResponseFactory
*/
@@ -529,7 +547,9 @@ public function toResponse($request = null)
}
if ($this->code) {
- $this->setStatusCode(is_int($this->code()) ? $this->code() : self::REST_RESPONSE_SUCCESS_CODE);
+ if (in_array($this->code(), static::CODES)) {
+ $this->setStatusCode(is_int($this->code()) ? $this->code() : self::REST_RESPONSE_SUCCESS_CODE);
+ }
}
if ($this->debug) {
diff --git a/src/Events/RestifyRepositoryStored.php b/src/Events/RestifyRepositoryStored.php
new file mode 100644
index 000000000..ced0c8fc6
--- /dev/null
+++ b/src/Events/RestifyRepositoryStored.php
@@ -0,0 +1,23 @@
+model = $model;
+ }
+}
diff --git a/src/Exceptions/InstanceOfException.php b/src/Exceptions/InstanceOfException.php
index a26d0f1c0..42ec0cba6 100644
--- a/src/Exceptions/InstanceOfException.php
+++ b/src/Exceptions/InstanceOfException.php
@@ -9,4 +9,8 @@
*/
class InstanceOfException extends Exception
{
+ public static function because($message = '')
+ {
+ return new static($message);
+ }
}
diff --git a/src/Exceptions/RestifyHandler.php b/src/Exceptions/RestifyHandler.php
index c8b1ec8b0..9b28c56d2 100644
--- a/src/Exceptions/RestifyHandler.php
+++ b/src/Exceptions/RestifyHandler.php
@@ -21,6 +21,7 @@
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
@@ -81,7 +82,10 @@ public function render($request, $exception)
break;
case $exception instanceof ValidationException:
- $response->errors($exception->errors())->invalid();
+ $response->addError($exception->errors())->invalid();
+ break;
+ case $exception instanceof InstanceOfException:
+ $response->errors($exception->getMessage())->invalid();
break;
case $exception instanceof MethodNotAllowedHttpException:
@@ -104,6 +108,9 @@ public function render($request, $exception)
case $exception instanceof InvalidSignatureException:
$response->addError($exception->getMessage())->forbidden();
break;
+ case $exception instanceof HttpException:
+ $response->addError($exception->getMessage())->forbidden();
+ break;
default:
if (App::environment('production') === true) {
diff --git a/src/Fields/BaseField.php b/src/Fields/BaseField.php
index 73b0e6f18..366ce3db3 100644
--- a/src/Fields/BaseField.php
+++ b/src/Fields/BaseField.php
@@ -2,42 +2,6 @@
namespace Binaryk\LaravelRestify\Fields;
-use Illuminate\Http\Request;
-
-/**
- * @author Eduard Lupacescu
- */
abstract class BaseField
{
- /**
- * Conditionally load the field.
- *
- * @var bool|callable
- */
- public $when = true;
-
- /**
- * Conditionally load the field.
- *
- * @param callable|bool $condition
- * @param bool $default
- * @return $this
- */
- public function when($condition, $default = false)
- {
- $this->when = $condition ?? $default;
-
- return $this;
- }
-
- /**
- * Conditionally load the field.
- *
- * @param Request $request
- * @return bool|callable|mixed
- */
- public function filter(Request $request)
- {
- return is_callable($this->when) ? call_user_func($this->when, $request) : $this->when;
- }
}
diff --git a/src/Fields/Field.php b/src/Fields/Field.php
index 29173609b..6564b619b 100644
--- a/src/Fields/Field.php
+++ b/src/Fields/Field.php
@@ -3,8 +3,11 @@
namespace Binaryk\LaravelRestify\Fields;
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
+use Binaryk\LaravelRestify\Repositories\Repository;
+use Binaryk\LaravelRestify\Traits\Make;
use Closure;
use Illuminate\Contracts\Validation\Rule;
+use Illuminate\Support\Str;
use JsonSerializable;
/**
@@ -12,12 +15,46 @@
*/
class Field extends OrganicField implements JsonSerializable
{
+ use Make;
+
+ /**
+ * The resource associated with the field.
+ *
+ * @var Repository
+ */
+ public $repository;
+
/**
* Column name of the field.
* @var string|callable|null
*/
public $attribute;
+ /**
+ * Field value.
+ *
+ * @var string|callable|null
+ */
+ public $value;
+
+ /**
+ * In case of the update, this will keep the previous value.
+ * @var
+ */
+ public $valueBeforeUpdate;
+
+ /**
+ * Closure to resolve the index method.
+ *
+ * @var
+ */
+ private $indexCallback;
+
+ /**
+ * @var Closure
+ */
+ public $showCallback;
+
/**
* Callback called when the value is filled, this callback will do not override the fill action.
* @var Closure
@@ -25,59 +62,76 @@ class Field extends OrganicField implements JsonSerializable
public $storeCallback;
/**
+ * Callback called when update.
* @var Closure
*/
- public $showCallback;
+ public $updateCallback;
/**
- * Callback called when trying to fill this attribute, this callback will override the fill action, so make
- * sure you assign the attribute to the model over this callback.
+ * Closure be used to resolve the field's value.
+ *
+ * @var \Closure
+ */
+ public $resolveCallback;
+
+ /**
+ * Callback called when trying to fill this attribute, this callback will override the storeCallback or updateCallback.
+ *
+ * Make sure you assign the attribute to the model over this callback.
*
* @var Closure
*/
public $fillCallback;
/**
- * Create a new field.
+ * Closure be used for computed field.
*
- * @param string|callable|null $attribute
+ * @var callable
*/
- public function __construct($attribute)
- {
- $this->attribute = $attribute;
- }
+ protected $computedCallback;
/**
- * Create a new element.
+ * Closure be used for the field's default value.
*
- * @param array $arguments
- * @return static
+ * @var callable
*/
- public static function make(...$arguments)
- {
- return new static(...$arguments);
- }
+ protected $defaultCallback;
/**
- * {@inheritdoc}
+ * Closure be used to be called after the field value stored.
*/
- public function jsonSerialize()
- {
- return [
- 'value' => $this->value,
- ];
- }
+ public $afterStoreCallback;
+
+ /**
+ * Closure be used to be called after the field value changed.
+ */
+ public $afterUpdateCallback;
/**
- * Callback called when the value is filled, this callback will do not override the fill action. If fillCallback is defined
- * this will do not be called.
+ * Create a new field.
*
- * @param Closure $callback
- * @return Field
+ * @param string|callable|null $attribute
+ * @param callable|null $resolveCallback
*/
- public function storeCallback(Closure $callback)
+ public function __construct($attribute, callable $resolveCallback = null)
{
- $this->storeCallback = $callback;
+ $this->attribute = $attribute;
+
+ $this->resolveCallback = $resolveCallback;
+
+ $this->default(null);
+
+ if ($attribute instanceof Closure || (is_callable($attribute) && is_object($attribute))) {
+ $this->computedCallback = $attribute;
+ $this->attribute = 'Computed';
+ } else {
+ $this->attribute = $attribute ?? str_replace(' ', '_', Str::lower($attribute));
+ }
+ }
+
+ public function indexCallback(Closure $callback)
+ {
+ $this->indexCallback = $callback;
return $this;
}
@@ -93,6 +147,20 @@ public function showCallback(Closure $callback)
return $this;
}
+ public function storeCallback(Closure $callback)
+ {
+ $this->storeCallback = $callback;
+
+ return $this;
+ }
+
+ public function updateCallback(Closure $callback)
+ {
+ $this->updateCallback = $callback;
+
+ return $this;
+ }
+
/**
* Callback called when trying to fill this attribute, this callback will override the fill action, so make
* sure you assign the attribute to the model over this callback.
@@ -116,13 +184,27 @@ public function fillCallback(Closure $callback)
*/
public function fillAttribute(RestifyRequest $request, $model)
{
+ $this->resolveValueBeforeUpdate($request, $model);
+
if (isset($this->fillCallback)) {
return call_user_func(
$this->fillCallback, $request, $model, $this->attribute
);
}
- return $this->fillAttributeFromRequest(
+ if ($request->isStoreRequest() && is_callable($this->storeCallback)) {
+ return call_user_func(
+ $this->storeCallback, $request, $model, $this->attribute
+ );
+ }
+
+ if ($request->isUpdateRequest() && is_callable($this->updateCallback)) {
+ return call_user_func(
+ $this->updateCallback, $request, $model, $this->attribute
+ );
+ }
+
+ $this->fillAttributeFromRequest(
$request, $model, $this->attribute
);
}
@@ -137,9 +219,7 @@ public function fillAttribute(RestifyRequest $request, $model)
protected function fillAttributeFromRequest(RestifyRequest $request, $model, $attribute)
{
if ($request->exists($attribute) || $request->get($attribute)) {
- $value = $request[$attribute] ?? $request->get($attribute);
-
- $model->{$attribute} = is_callable($this->storeCallback) ? call_user_func($this->storeCallback, $value, $request, $model) : $value;
+ $model->{$attribute} = $request[$attribute] ?? $request->get($attribute);
}
}
@@ -163,6 +243,17 @@ public function storingRules($rules)
return $this;
}
+ /**
+ * Alias for storingRules - to maintain it consistent.
+ *
+ * @param $rules
+ * @return $this
+ */
+ public function storeRules($rules)
+ {
+ return $this->storingRules($rules);
+ }
+
/**
* Validation rules for update.
*
@@ -188,12 +279,6 @@ public function rules($rules)
return $this;
}
- /**
- * Validation messages.
- *
- * @param array $messages
- * @return Field
- */
public function messages(array $messages)
{
$this->messages = $messages;
@@ -201,20 +286,12 @@ public function messages(array $messages)
return $this;
}
- /**
- * Validation rules for storing.
- *
- * @return array
- */
- public function getStoringRules()
+ public function getStoringRules(): array
{
return array_merge($this->rules, $this->storingRules);
}
- /**
- * @return array
- */
- public function getUpdatingRules()
+ public function getUpdatingRules(): array
{
return array_merge($this->rules, $this->updatingRules);
}
@@ -224,22 +301,72 @@ public function getUpdatingRules()
*
* @param mixed $repository
* @param string|null $attribute
- * @return callable|string
+ * @return Field|void
*/
public function resolveForShow($repository, $attribute = null)
{
$attribute = $attribute ?? $this->attribute;
- if (is_callable($this->showCallback)) {
- $value = $this->resolveAttribute($repository, $attribute);
- $attribute = call_user_func($this->showCallback, $value, $repository, $attribute);
+ if ($attribute === 'Computed') {
+ $this->value = call_user_func($this->computedCallback, $repository);
+
+ return;
+ }
+
+ if (! $this->showCallback) {
+ $this->resolve($repository, $attribute);
+ } elseif (is_callable($this->showCallback)) {
+ tap($this->value ?? $this->resolveAttribute($repository, $attribute), function ($value) use ($repository, $attribute) {
+ $this->value = call_user_func($this->showCallback, $value, $repository, $attribute);
+ });
+ }
+
+ return $this;
+ }
+
+ public function resolveForIndex($repository, $attribute = null)
+ {
+ $this->repository = $repository;
+
+ $attribute = $attribute ?? $this->attribute;
+
+ if ($attribute === 'Computed') {
+ $this->value = call_user_func($this->computedCallback, $repository);
+
+ return;
}
- return $attribute;
+ if (! $this->indexCallback) {
+ $this->resolve($repository, $attribute);
+ } elseif (is_callable($this->indexCallback)) {
+ tap($this->value ?? $this->resolveAttribute($repository, $attribute), function ($value) use ($repository, $attribute) {
+ $this->value = call_user_func($this->indexCallback, $value, $repository, $attribute);
+ });
+ }
+
+ return $this;
+ }
+
+ public function resolve($repository, $attribute = null)
+ {
+ $this->repository = $repository;
+ if ($attribute === 'Computed') {
+ $this->value = call_user_func($this->computedCallback, $repository);
+
+ return;
+ }
+
+ if (! $this->resolveCallback) {
+ $this->value = $this->resolveAttribute($repository, $attribute);
+ } elseif (is_callable($this->resolveCallback)) {
+ tap($this->resolveAttribute($repository, $attribute), function ($value) use ($repository, $attribute) {
+ $this->value = call_user_func($this->resolveCallback, $value, $repository, $attribute);
+ });
+ }
}
/**
- * Resolve the given attribute from the given resource.
+ * Resolve the given attribute from the given repository.
*
* @param mixed $repository
* @param string $attribute
@@ -249,4 +376,94 @@ protected function resolveAttribute($repository, $attribute)
{
return data_get($repository, str_replace('->', '.', $attribute));
}
+
+ protected function resolveValueBeforeUpdate(RestifyRequest $request, $repository)
+ {
+ if ($request->isUpdateRequest()) {
+ $this->valueBeforeUpdate = $this->resolveAttribute($repository, $this->attribute);
+ }
+ }
+
+ public function jsonSerialize()
+ {
+ return with(app(RestifyRequest::class), function ($request) {
+ return [
+ 'attribute' => $this->attribute,
+ 'value' => $this->resolveDefaultValue($request) ?? $this->value,
+ ];
+ });
+ }
+
+ public function serializeToValue($request)
+ {
+ return [
+ $this->attribute => $this->resolveDefaultValue($request) ?? $this->value,
+ ];
+ }
+
+ /**
+ * Set the callback to be used for determining the field's default value.
+ *
+ * @param $callback
+ * @return $this
+ */
+ public function default($callback)
+ {
+ $this->defaultCallback = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Resolve the default value for the field.
+ *
+ * @param RestifyRequest $request
+ * @return callable|mixed
+ */
+ protected function resolveDefaultValue(RestifyRequest $request)
+ {
+ if (is_null($this->value) && is_callable($this->defaultCallback)) {
+ return call_user_func($this->defaultCallback, $request);
+ }
+
+ return $this->defaultCallback;
+ }
+
+ /**
+ * Define the callback that should be used to resolve the field's value.
+ *
+ * @param callable $resolveCallback
+ * @return $this
+ */
+ public function resolveUsing(callable $resolveCallback)
+ {
+ $this->resolveCallback = $resolveCallback;
+
+ return $this;
+ }
+
+ public function afterUpdate(Closure $callback)
+ {
+ $this->afterUpdateCallback = $callback;
+
+ return $this;
+ }
+
+ public function afterStore(Closure $callback)
+ {
+ $this->afterStoreCallback = $callback;
+
+ return $this;
+ }
+
+ public function invokeAfter(RestifyRequest $request, $repository)
+ {
+ if ($request->isStoreRequest() && is_callable($this->afterStoreCallback)) {
+ call_user_func($this->afterStoreCallback, data_get($repository, $this->attribute), $repository, $request);
+ }
+
+ if ($request->isUpdateRequest() && is_callable($this->afterUpdateCallback)) {
+ call_user_func($this->afterUpdateCallback, $this->resolveAttribute($repository, $this->attribute), $this->valueBeforeUpdate, $repository, $request);
+ }
+ }
}
diff --git a/src/Fields/FieldCollection.php b/src/Fields/FieldCollection.php
new file mode 100644
index 000000000..152afe843
--- /dev/null
+++ b/src/Fields/FieldCollection.php
@@ -0,0 +1,66 @@
+filter(function (OrganicField $field) use ($request) {
+ return $field->authorize($request);
+ })->values();
+ }
+
+ public function authorizedUpdate(Request $request): self
+ {
+ return $this->filter(function (OrganicField $field) use ($request) {
+ return $field->authorizedToUpdate($request);
+ })->values();
+ }
+
+ public function authorizedStore(Request $request): self
+ {
+ return $this->filter(function (OrganicField $field) use ($request) {
+ return $field->authorizedToStore($request);
+ })->values();
+ }
+
+ public function resolve($repository): self
+ {
+ return $this->each(function ($field) use ($repository) {
+ $field->resolve($repository);
+ });
+ }
+
+ public function forIndex(RestifyRequest $request, $repository): self
+ {
+ return $this->filter(function (Field $field) use ($repository, $request) {
+ return $field->isShownOnIndex($request, $repository);
+ })->values();
+ }
+
+ public function forShow(RestifyRequest $request, $repository): self
+ {
+ return $this->filter(function (Field $field) use ($repository, $request) {
+ return $field->isShownOnShow($request, $repository);
+ })->values();
+ }
+
+ public function forStore(RestifyRequest $request, $repository): self
+ {
+ return $this->filter(function (Field $field) use ($repository, $request) {
+ return $field->isShownOnStore($request, $repository);
+ })->values();
+ }
+
+ public function forUpdate(RestifyRequest $request, $repository): self
+ {
+ return $this->filter(function (Field $field) use ($repository, $request) {
+ return $field->isShownOnUpdate($request, $repository);
+ })->values();
+ }
+}
diff --git a/src/Fields/OrganicField.php b/src/Fields/OrganicField.php
index 385f2b28e..a6c03b3f9 100644
--- a/src/Fields/OrganicField.php
+++ b/src/Fields/OrganicField.php
@@ -3,70 +3,38 @@
namespace Binaryk\LaravelRestify\Fields;
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
+use Closure;
+use Illuminate\Http\Request;
-/**
- * @author Eduard Lupacescu
- */
abstract class OrganicField extends BaseField
{
- /**
- * Rules for applied when store.
- *
- * @var array
- */
- public $storingRules = [];
-
- /**
- * Rules for applied when update model.
- * @var array
- */
- public $updatingRules = [];
-
- /**
- * Rules for applied when store and update.
- *
- * @var array
- */
- public $rules = [];
-
- /**
- * @var array
- */
- public $messages = [];
-
- /**
- * Indicates if the element should be shown on the index view.
- *
- * @var \Closure|bool
- */
+ public $canSeeCallback;
+
+ public $canUpdateCallback;
+
+ public $canStoreCallback;
+
+ public $readonlyCallback;
+
+ public array $rules = [];
+
+ public array $storingRules = [];
+
+ public array $updatingRules = [];
+
+ public array $messages = [];
+
public $showOnIndex = true;
- /**
- * Indicates if the element should be shown on the detail view.
- *
- * @var \Closure|bool
- */
- public $showOnDetail = true;
+ public $showOnShow = true;
- /**
- * Specify that the element should be hidden from the detail view.
- *
- * @param \Closure|bool $callback
- * @return $this
- */
- public function showOnDetail($callback = true)
+ public function showOnShow($callback = true)
{
- $this->showOnDetail = $callback;
+ $this->showOnShow = $callback;
return $this;
}
- /**
- * Specify that the element should be hidden from the detail view.
- *
- * @param \Closure|bool $callback
- * @return $this
- */
public function showOnIndex($callback = true)
{
$this->showOnIndex = $callback;
@@ -74,15 +42,9 @@ public function showOnIndex($callback = true)
return $this;
}
- /**
- * Specify that the element should be hidden from the detail view.
- *
- * @param \Closure|bool $callback
- * @return $this
- */
- public function hideFromDetail($callback = true)
+ public function hideFromShow($callback = true)
{
- $this->showOnDetail = is_callable($callback) ? function () use ($callback) {
+ $this->showOnShow = is_callable($callback) ? function () use ($callback) {
return ! call_user_func_array($callback, func_get_args());
}
: ! $callback;
@@ -90,12 +52,6 @@ public function hideFromDetail($callback = true)
return $this;
}
- /**
- * Specify that the element should be hidden from the index view.
- *
- * @param \Closure|bool $callback
- * @return $this
- */
public function hideFromIndex($callback = true)
{
$this->showOnIndex = is_callable($callback) ? function () use ($callback) {
@@ -106,45 +62,25 @@ public function hideFromIndex($callback = true)
return $this;
}
- /**
- * Check showing on detail.
- *
- * @param RestifyRequest $request
- * @param mixed $repository
- * @return bool
- */
- public function isShownOnDetail(RestifyRequest $request, $repository): bool
+ public function isShownOnShow(RestifyRequest $request, $repository): bool
{
- if (is_callable($this->showOnDetail)) {
- $this->showOnDetail = call_user_func($this->showOnDetail, $request, $repository);
+ if (is_callable($this->showOnShow)) {
+ $this->showOnShow = call_user_func($this->showOnShow, $request, $repository);
}
- return $this->showOnDetail;
+ return $this->showOnShow;
}
- /**
- * Check hidden on detail.
- *
- * @param RestifyRequest $request
- * @param mixed $repository
- * @return bool
- */
- public function isHiddenOnDetail(RestifyRequest $request, $repository): bool
+ public function isHiddenOnShow(RestifyRequest $request, $repository): bool
{
- if (is_callable($this->showOnDetail)) {
- $this->showOnDetail = call_user_func($this->showOnDetail, $request, $repository);
- }
+ return false === $this->isShownOnShow($request, $repository);
+ }
- return ! $this->showOnDetail;
+ public function isShownOnIndex(RestifyRequest $request, $repository): bool
+ {
+ return false === $this->isHiddenOnIndex($request, $repository);
}
- /**
- * Check hidden on index.
- *
- * @param RestifyRequest $request
- * @param mixed $repository
- * @return bool
- */
public function isHiddenOnIndex(RestifyRequest $request, $repository): bool
{
if (is_callable($this->showOnIndex)) {
@@ -153,4 +89,73 @@ public function isHiddenOnIndex(RestifyRequest $request, $repository): bool
return ! $this->showOnIndex;
}
+
+ public function authorize(Request $request)
+ {
+ return $this->authorizedToSee($request);
+ }
+
+ public function authorizedToSee(Request $request)
+ {
+ return $this->canSeeCallback ? call_user_func($this->canSeeCallback, $request) : true;
+ }
+
+ public function authorizedToUpdate(Request $request)
+ {
+ return $this->canUpdateCallback ? call_user_func($this->canUpdateCallback, $request) : true;
+ }
+
+ public function authorizedToStore(Request $request)
+ {
+ return $this->canStoreCallback ? call_user_func($this->canStoreCallback, $request) : true;
+ }
+
+ public function canSee(Closure $callback)
+ {
+ $this->canSeeCallback = $callback;
+
+ return $this;
+ }
+
+ public function canUpdate(Closure $callback)
+ {
+ $this->canUpdateCallback = $callback;
+
+ return $this;
+ }
+
+ public function canStore(Closure $callback)
+ {
+ $this->canStoreCallback = $callback;
+
+ return $this;
+ }
+
+ public function readonly($callback = true)
+ {
+ $this->readonlyCallback = $callback;
+
+ return $this;
+ }
+
+ public function isReadonly(RestifyRequest $request)
+ {
+ return with($this->readonlyCallback, function ($callback) use ($request) {
+ if ($callback === true || (is_callable($callback) && call_user_func($callback, $request))) {
+ return true;
+ }
+
+ return false;
+ });
+ }
+
+ public function isShownOnUpdate(RestifyRequest $request, $repository): bool
+ {
+ return ! $this->isReadonly($request);
+ }
+
+ public function isShownOnStore(RestifyRequest $request, $repository): bool
+ {
+ return ! $this->isReadonly($request);
+ }
}
diff --git a/src/Http/Controllers/RepositoryDestroyController.php b/src/Http/Controllers/RepositoryDestroyController.php
index 9d26663b9..252c76203 100644
--- a/src/Http/Controllers/RepositoryDestroyController.php
+++ b/src/Http/Controllers/RepositoryDestroyController.php
@@ -2,35 +2,13 @@
namespace Binaryk\LaravelRestify\Http\Controllers;
-use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException;
-use Binaryk\LaravelRestify\Exceptions\UnauthorizedException;
use Binaryk\LaravelRestify\Http\Requests\RepositoryDestroyRequest;
-use Binaryk\LaravelRestify\Repositories\Repository;
-use Illuminate\Auth\Access\AuthorizationException;
-use Illuminate\Contracts\Container\BindingResolutionException;
-use Illuminate\Http\JsonResponse;
-use Throwable;
-/**
- * @author Eduard Lupacescu
- */
class RepositoryDestroyController extends RepositoryController
{
- /**
- * @param RepositoryDestroyRequest $request
- * @return JsonResponse
- * @throws AuthorizationException
- * @throws BindingResolutionException
- * @throws EntityNotFoundException
- * @throws Throwable
- * @throws UnauthorizedException
- */
- public function handle(RepositoryDestroyRequest $request)
+ public function __invoke(RepositoryDestroyRequest $request)
{
- /**
- * @var Repository
- */
- $repository = $request->newRepositoryWith($request->findModelQuery()->firstOrFail());
+ $repository = $request->newRepositoryWith($request->findModelQuery()->firstOrFail())->allowToDestroy($request);
return $repository->destroy($request, request('repositoryId'));
}
diff --git a/src/Http/Controllers/RepositoryIndexController.php b/src/Http/Controllers/RepositoryIndexController.php
index c186a3a98..f8127fe55 100644
--- a/src/Http/Controllers/RepositoryIndexController.php
+++ b/src/Http/Controllers/RepositoryIndexController.php
@@ -3,25 +3,12 @@
namespace Binaryk\LaravelRestify\Http\Controllers;
use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException;
-use Binaryk\LaravelRestify\Exceptions\InstanceOfException;
use Binaryk\LaravelRestify\Exceptions\UnauthorizedException;
-use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
+use Binaryk\LaravelRestify\Http\Requests\RepositoryIndexRequest;
-/**
- * @author Eduard Lupacescu
- */
class RepositoryIndexController extends RepositoryController
{
- /**
- * @param RestifyRequest $request
- * @return \Binaryk\LaravelRestify\Repositories\Repository
- * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException
- * @throws \Binaryk\LaravelRestify\Exceptions\InstanceOfException
- * @throws \Binaryk\LaravelRestify\Exceptions\UnauthorizedException
- * @throws \Illuminate\Contracts\Container\BindingResolutionException
- * @throws \Throwable
- */
- public function handle(RestifyRequest $request)
+ public function __invoke(RepositoryIndexRequest $request)
{
try {
return $request->newRepository()->index($request);
@@ -31,7 +18,7 @@ public function handle(RestifyRequest $request)
->dump($e, $request->isDev());
} catch (UnauthorizedException $e) {
return $this->response()->forbidden()->addError($e->getMessage())->dump($e, $request->isDev());
- } catch (InstanceOfException | \Throwable $e) {
+ } catch (\Throwable $e) {
return $this->response()->error()->dump($e, $request->isDev());
}
}
diff --git a/src/Http/Controllers/RepositoryShowController.php b/src/Http/Controllers/RepositoryShowController.php
index f9b78be6e..70b8c380c 100644
--- a/src/Http/Controllers/RepositoryShowController.php
+++ b/src/Http/Controllers/RepositoryShowController.php
@@ -2,21 +2,14 @@
namespace Binaryk\LaravelRestify\Http\Controllers;
-use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
+use Binaryk\LaravelRestify\Http\Requests\RepositoryShowRequest;
-/**
- * @author Eduard Lupacescu
- */
class RepositoryShowController extends RepositoryController
{
- /**
- * @param RestifyRequest $request
- * @return \Binaryk\LaravelRestify\Controllers\RestResponse|mixed
- * @throws \Illuminate\Auth\Access\AuthorizationException
- * @throws \Throwable
- */
- public function handle(RestifyRequest $request)
+ public function __invoke(RepositoryShowRequest $request)
{
- return $request->newRepositoryWith($request->findModelQuery())->show($request, request('repositoryId'));
+ return $request->newRepositoryWith($request->findModelQuery()->firstOrFail())
+ ->allowToShow($request)
+ ->show($request, request('repositoryId'));
}
}
diff --git a/src/Http/Controllers/RepositoryStoreController.php b/src/Http/Controllers/RepositoryStoreController.php
index f14aa555f..caa7d4372 100644
--- a/src/Http/Controllers/RepositoryStoreController.php
+++ b/src/Http/Controllers/RepositoryStoreController.php
@@ -2,36 +2,14 @@
namespace Binaryk\LaravelRestify\Http\Controllers;
-use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException;
-use Binaryk\LaravelRestify\Exceptions\UnauthorizedException;
use Binaryk\LaravelRestify\Http\Requests\RepositoryStoreRequest;
-use Binaryk\LaravelRestify\Repositories\Repository;
-use Illuminate\Auth\Access\AuthorizationException;
-use Illuminate\Contracts\Container\BindingResolutionException;
-use Illuminate\Http\JsonResponse;
-use Throwable;
-/**
- * @author Eduard Lupacescu
- */
class RepositoryStoreController extends RepositoryController
{
- /**
- * @param RepositoryStoreRequest $request
- * @return JsonResponse
- * @throws BindingResolutionException
- * @throws EntityNotFoundException
- * @throws UnauthorizedException
- * @throws AuthorizationException
- * @throws Throwable
- */
- public function handle(RepositoryStoreRequest $request)
+ public function __invoke(RepositoryStoreRequest $request)
{
- /**
- * @var Repository
- */
- $repository = $request->repository();
-
- return $request->newRepositoryWith($repository::newModel())->store($request);
+ return $request->repository()
+ ->allowToStore($request)
+ ->store($request);
}
}
diff --git a/src/Http/Controllers/RepositoryUpdateController.php b/src/Http/Controllers/RepositoryUpdateController.php
index 22394c3b3..0368e8949 100644
--- a/src/Http/Controllers/RepositoryUpdateController.php
+++ b/src/Http/Controllers/RepositoryUpdateController.php
@@ -2,39 +2,18 @@
namespace Binaryk\LaravelRestify\Http\Controllers;
-use Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException;
-use Binaryk\LaravelRestify\Exceptions\UnauthorizedException;
-use Binaryk\LaravelRestify\Http\Requests\RepositoryStoreRequest;
use Binaryk\LaravelRestify\Http\Requests\RepositoryUpdateRequest;
use Binaryk\LaravelRestify\Repositories\Repository;
-use Illuminate\Auth\Access\AuthorizationException;
-use Illuminate\Contracts\Container\BindingResolutionException;
-use Illuminate\Http\JsonResponse;
-use Throwable;
-/**
- * @author Eduard Lupacescu
- */
class RepositoryUpdateController extends RepositoryController
{
- /**
- * @param RepositoryStoreRequest $request
- * @return JsonResponse
- * @throws BindingResolutionException
- * @throws EntityNotFoundException
- * @throws UnauthorizedException
- * @throws AuthorizationException
- * @throws Throwable
- */
- public function handle(RepositoryUpdateRequest $request)
+ public function __invoke(RepositoryUpdateRequest $request)
{
$model = $request->findModelQuery()->lockForUpdate()->firstOrFail();
- /**
- * @var Repository
- */
+ /** * @var Repository $repository */
$repository = $request->newRepositoryWith($model);
- return $repository->update($request, request('repositoryId'));
+ return $repository->allowToUpdate($request)->update($request, request('repositoryId'));
}
}
diff --git a/src/Http/Requests/InteractWithRepositories.php b/src/Http/Requests/InteractWithRepositories.php
index 10684ccb1..8513ae7b5 100644
--- a/src/Http/Requests/InteractWithRepositories.php
+++ b/src/Http/Requests/InteractWithRepositories.php
@@ -34,21 +34,24 @@ public function authorize()
* @param null $key
* @return Repository
*/
- public function repository($key = null)
+ public function repository($key = null): ?Repository
{
- return tap(Restify::repositoryForKey($key ?? $this->route('repository')), function ($repository) {
+ $repository = tap(Restify::repositoryForKey($key ?? $this->route('repository')), function ($repository) {
+ /** * @var Repository $repository */
if (is_null($repository)) {
throw new EntityNotFoundException(__('Repository :name not found.', [
'name' => $repository,
]), 404);
}
- if (! $repository::authorizedToShowAny($this)) {
- throw new UnauthorizedException(__('Unauthorized to view repository :name. See "showAny" policy.', [
+ if (! $repository::authorizedToUseRepository($this)) {
+ throw new UnauthorizedException(__('Unauthorized to view repository :name. See "allowRestify" policy.', [
'name' => $repository,
]), 403);
}
});
+
+ return $repository::resolveWith($repository::newModel());
}
/**
@@ -66,8 +69,8 @@ public function rules()
/**
* Get the route handling the request.
*
- * @param string|null $param
- * @param mixed $default
+ * @param string|null $param
+ * @param mixed $default
* @return \Illuminate\Routing\Route|object|string
*/
abstract public function route($param = null, $default = null);
diff --git a/src/Http/Requests/ResourceRequest.php b/src/Http/Requests/RepositoryShowRequest.php
similarity index 71%
rename from src/Http/Requests/ResourceRequest.php
rename to src/Http/Requests/RepositoryShowRequest.php
index a78231bd4..383777564 100644
--- a/src/Http/Requests/ResourceRequest.php
+++ b/src/Http/Requests/RepositoryShowRequest.php
@@ -5,6 +5,6 @@
/**
* @author Eduard Lupacescu
*/
-class ResourceRequest extends RestifyRequest
+class RepositoryShowRequest extends RestifyRequest
{
}
diff --git a/src/Http/Requests/RestifyRequest.php b/src/Http/Requests/RestifyRequest.php
index ce4a5cac8..5cc6cff1d 100644
--- a/src/Http/Requests/RestifyRequest.php
+++ b/src/Http/Requests/RestifyRequest.php
@@ -2,7 +2,6 @@
namespace Binaryk\LaravelRestify\Http\Requests;
-use Binaryk\LaravelRestify\Restify;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\App;
@@ -36,9 +35,7 @@ public function isDev()
*/
public function isIndexRequest()
{
- $path = trim(Restify::path($this->route('repository')), '/') ?: '/';
-
- return $this->is($path);
+ return $this instanceof RepositoryIndexRequest;
}
/**
@@ -46,8 +43,18 @@ public function isIndexRequest()
* This will match any verbs (PATCH, DELETE or GET).
* @return bool
*/
- public function isDetailRequest()
+ public function isShowRequest()
+ {
+ return $this instanceof RepositoryShowRequest;
+ }
+
+ public function isUpdateRequest()
+ {
+ return $this instanceof RepositoryUpdateRequest;
+ }
+
+ public function isStoreRequest()
{
- return ! $this->isIndexRequest();
+ return $this instanceof RepositoryStoreRequest;
}
}
diff --git a/src/LaravelRestifyServiceProvider.php b/src/LaravelRestifyServiceProvider.php
index 799b73214..8583871d6 100644
--- a/src/LaravelRestifyServiceProvider.php
+++ b/src/LaravelRestifyServiceProvider.php
@@ -8,6 +8,7 @@
use Binaryk\LaravelRestify\Commands\Refresh;
use Binaryk\LaravelRestify\Commands\RepositoryCommand;
use Binaryk\LaravelRestify\Commands\SetupCommand;
+use Binaryk\LaravelRestify\Commands\StubCommand;
use Binaryk\LaravelRestify\Http\Middleware\RestifyInjector;
use Illuminate\Contracts\Http\Kernel as HttpKernel;
use Illuminate\Support\ServiceProvider;
@@ -26,6 +27,7 @@ public function boot()
PolicyCommand::class,
BaseRepositoryCommand::class,
Refresh::class,
+ StubCommand::class,
]);
$this->registerPublishing();
diff --git a/src/Repositories/Crudable.php b/src/Repositories/Crudable.php
deleted file mode 100644
index c45d79a64..000000000
--- a/src/Repositories/Crudable.php
+++ /dev/null
@@ -1,349 +0,0 @@
-
- */
-trait Crudable
-{
- /**
- * @param RestifyRequest $request
- * @return JsonResponse
- * @throws \Binaryk\LaravelRestify\Exceptions\InstanceOfException
- * @throws \Throwable
- */
- public function index(RestifyRequest $request)
- {
- $results = SearchService::instance()->search($request, $this->model());
-
- $results = $results->tap(function ($query) use ($request) {
- static::indexQuery($request, $query);
- });
-
- /**
- * @var AbstractPaginator
- */
- $paginator = $results->paginate($request->get('perPage') ?? (static::$defaultPerPage ?? RestifySearchable::DEFAULT_PER_PAGE));
-
- $items = $paginator->getCollection()->map(function ($value) {
- return static::resolveWith($value);
- });
-
- try {
- $this->allowToShowEvery($request, $items);
- } catch (UnauthorizedException | AuthorizationException $e) {
- return $this->response()->forbidden()->addError($e->getMessage());
- }
-
- // Filter out items the request user don't have enough permissions for show
- $items = $items->filter(function ($repository) use ($request) {
- return $repository->authorizedToShow($request);
- });
-
- return $this->response([
- 'meta' => RepositoryCollection::meta($paginator->toArray()),
- 'links' => RepositoryCollection::paginationLinks($paginator->toArray()),
- 'data' => $items,
- ]);
- }
-
- /**
- * @param RestifyRequest $request
- * @param $repositoryId
- * @return JsonResponse
- * @throws AuthorizationException
- * @throws UnauthorizedException
- * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException
- */
- public function show(RestifyRequest $request, $repositoryId)
- {
- $this->resource = static::showPlain($repositoryId);
-
- try {
- $this->allowToShow($request);
- } catch (AuthorizationException $e) {
- return $this->response()->forbidden()->addError($e->getMessage());
- }
-
- return $this->response()->data($this->jsonSerialize());
- }
-
- /**
- * @param RestifyRequest $request
- * @return JsonResponse
- * @throws AuthorizationException
- * @throws ValidationException
- */
- public function store(RestifyRequest $request)
- {
- try {
- $this->allowToStore($request);
- } catch (AuthorizationException | UnauthorizedException $e) {
- return $this->response()->addError($e->getMessage())->code(RestResponse::REST_RESPONSE_FORBIDDEN_CODE);
- } catch (ValidationException $e) {
- return $this->response()->addError($e->errors())
- ->code(RestResponse::REST_RESPONSE_INVALID_CODE);
- }
-
- $this->resource = static::storePlain($request->toArray());
-
- static::stored($this->resource);
-
- return $this->response('', RestResponse::REST_RESPONSE_CREATED_CODE)
- ->model($this->resource)
- ->header('Location', Restify::path().'/'.static::uriKey().'/'.$this->resource->id);
- }
-
- /**
- * @param RestifyRequest $request
- * @param $repositoryId
- * @return JsonResponse
- * @throws AuthorizationException
- * @throws UnauthorizedException
- * @throws ValidationException
- * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException
- */
- public function update(RestifyRequest $request, $repositoryId)
- {
- $this->allowToUpdate($request);
-
- $this->resource = static::updatePlain($request->all(), $repositoryId);
-
- static::updated($this->resource);
-
- return $this->response()
- ->data($this->jsonSerialize())
- ->updated();
- }
-
- /**
- * @param RestifyRequest $request
- * @param $repositoryId
- * @return JsonResponse
- * @throws AuthorizationException
- * @throws UnauthorizedException
- * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException
- */
- public function destroy(RestifyRequest $request, $repositoryId)
- {
- $this->allowToDestroy($request);
-
- $status = static::destroyPlain($repositoryId);
-
- static::deleted($status);
-
- return $this->response()->deleted();
- }
-
- /**
- * @param RestifyRequest $request
- * @param array $payload
- * @return mixed
- */
- public function allowToUpdate(RestifyRequest $request, $payload = null)
- {
- $this->authorizeToUpdate($request);
-
- $validator = static::validatorForUpdate($request, $this, $payload);
-
- $validator->validate();
- }
-
- /**
- * @param RestifyRequest $request
- * @param array $payload
- * @return mixed
- */
- public function allowToStore(RestifyRequest $request, $payload = null)
- {
- static::authorizeToStore($request);
-
- $validator = static::validatorForStoring($request, $payload);
-
- $validator->validate();
- }
-
- /**
- * @param RestifyRequest $request
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function allowToDestroy(RestifyRequest $request)
- {
- $this->authorizeToDelete($request);
- }
-
- /**
- * @param $request
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function allowToShow($request)
- {
- $this->authorizeToShow($request);
- }
-
- /**
- * @param $request
- * @param Collection $items
- * @throws \Illuminate\Auth\Access\AuthorizationException
- */
- public function allowToShowEvery($request, Collection $items)
- {
- $this->authorizeToShowEvery($request);
- }
-
- /**
- * Validate input array and store a new entity.
- *
- * @param array $payload
- * @return mixed
- * @throws AuthorizationException
- * @throws ValidationException
- */
- public static function storePlain(array $payload)
- {
- /** * @var RepositoryStoreRequest $request */
- $request = resolve(RepositoryStoreRequest::class);
-
- $request->attributes->add($payload);
-
- $repository = resolve(static::class);
-
- $repository->allowToStore($request, $payload);
-
- return DB::transaction(function () use ($request) {
- $model = static::fillWhenStore(
- $request, static::newModel()
- );
-
- $model->save();
-
- return $model;
- });
- }
-
- /**
- * Update an entity with an array of payload.
- *
- * @param array $payload
- * @param $id
- * @return mixed
- * @throws AuthorizationException
- * @throws UnauthorizedException
- * @throws ValidationException
- * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException
- */
- public static function updatePlain(array $payload, $id)
- {
- /** * @var RepositoryUpdateRequest $request */
- $request = resolve(RepositoryUpdateRequest::class);
- $request->attributes->add($payload);
-
- $model = $request->findModelQuery($id, static::uriKey())->lockForUpdate()->firstOrFail();
-
- /**
- * @var Repository
- */
- $repository = $request->newRepositoryWith($model, static::uriKey());
-
- $repository->allowToUpdate($request, $payload);
-
- return DB::transaction(function () use ($request, $repository) {
- $model = static::fillWhenUpdate($request, $repository->resource);
-
- $model->save();
-
- return $model;
- });
- }
-
- /**
- * Returns a plain model by key
- * Used as: Book::showPlain(1).
- *
- * @param $key
- * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Model
- * @throws AuthorizationException
- * @throws UnauthorizedException
- * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException
- */
- public static function showPlain($key)
- {
- /** * @var RestifyRequest $request */
- $request = resolve(RestifyRequest::class);
-
- /**
- * Dive into the Search service to attach relations.
- */
- $repository = $request->newRepositoryWith(tap($request->findModelQuery($key, static::uriKey())->firstOrFail(), function ($query) use ($request) {
- static::detailQuery($request, $query);
- }));
-
- $repository->allowToShow($request);
-
- return $repository->resource;
- }
-
- /**
- * Validate deletion and delete entity.
- *
- * @param $key
- * @return mixed
- * @throws AuthorizationException
- * @throws UnauthorizedException
- * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException
- */
- public static function destroyPlain($key)
- {
- /** * @var RepositoryDestroyRequest $request */
- $request = resolve(RepositoryDestroyRequest::class);
-
- $repository = $request->newRepositoryWith($request->findModelQuery($key, static::uriKey())->firstOrFail(), static::uriKey());
-
- $repository->allowToDestroy($request);
-
- return DB::transaction(function () use ($repository) {
- return $repository->resource->delete();
- });
- }
-
- /**
- * @param $model
- */
- public static function stored($model)
- {
- //
- }
-
- /**
- * @param $model
- */
- public static function updated($model)
- {
- //
- }
-
- /**
- * @param int $status
- */
- public static function deleted($status)
- {
- //
- }
-}
diff --git a/src/Repositories/Mergeable.php b/src/Repositories/Mergeable.php
new file mode 100644
index 000000000..dcdd60436
--- /dev/null
+++ b/src/Repositories/Mergeable.php
@@ -0,0 +1,11 @@
+ users
+ * e.g. LaravelEntityRepository => laravel-entities.
+ */
+ return Str::plural($kebabWithoutRepository);
}
/**
@@ -74,7 +111,7 @@ public static function uriKey()
*
* @return mixed
*/
- public static function newModel()
+ public static function newModel(): Model
{
if (property_exists(static::class, 'model')) {
$model = static::$model;
@@ -85,33 +122,11 @@ public static function newModel()
return new $model;
}
- /**
- * @return Builder
- */
- public static function query()
+ public static function query(): Builder
{
return static::newModel()->query();
}
- /**
- * @param $request
- * @return array
- */
- public function toArray(RestifyRequest $request)
- {
- $serialized = [
- 'id' => $this->when($this->resource instanceof Model, function () {
- return $this->resource->getKey();
- }),
- 'type' => $this->model()->getTable(),
- 'attributes' => $this->resolveDetailsAttributes($request),
- 'relationships' => $this->when(value($this->resolveDetailsRelationships($request)), $this->resolveDetailsRelationships($request)),
- 'meta' => $this->when(value($this->resolveDetailsMeta($request)), $this->resolveDetailsMeta($request)),
- ];
-
- return $this->serializeDetails($request, $serialized);
- }
-
/**
* Resolvable attributes before storing/updating.
*
@@ -125,13 +140,67 @@ public function fields(RestifyRequest $request)
/**
* @param RestifyRequest $request
- * @return Collection
+ * @return FieldCollection
*/
public function collectFields(RestifyRequest $request)
{
- return collect($this->fields($request))->filter(function (Field $field) use ($request) {
- return $field->filter($request);
- });
+ $method = 'fields';
+
+ if ($request->isIndexRequest() && method_exists($this, 'fieldsForIndex')) {
+ $method = 'fieldsForIndex';
+ }
+
+ if ($request->isShowRequest() && method_exists($this, 'fieldsForShow')) {
+ $method = 'fieldsForShow';
+ }
+
+ if ($request->isUpdateRequest() && method_exists($this, 'fieldsForUpdate')) {
+ $method = 'fieldsForUpdate';
+ }
+
+ if ($request->isStoreRequest() && method_exists($this, 'fieldsForStore')) {
+ $method = 'fieldsForStore';
+ }
+
+ $fields = FieldCollection::make(array_values($this->filter($this->{$method}($request))));
+
+ if ($this instanceof Mergeable) {
+ $fillable = collect($this->resource->getFillable())
+ ->filter(fn ($attribute) => $fields->contains('attribute', $attribute) === false)
+ ->map(fn ($attribute) => Field::new($attribute));
+
+ $fields = $fields->merge($fillable);
+ }
+
+ return $fields;
+ }
+
+ private function indexFields(RestifyRequest $request): Collection
+ {
+ return $this->collectFields($request)
+ ->filter(fn (Field $field) => ! $field->isHiddenOnIndex($request, $this))
+ ->values();
+ }
+
+ private function showFields(RestifyRequest $request): Collection
+ {
+ return $this->collectFields($request)
+ ->filter(fn (Field $field) => ! $field->isHiddenOnShow($request, $this))
+ ->values();
+ }
+
+ private function updateFields(RestifyRequest $request)
+ {
+ return $this->collectFields($request)
+ ->forUpdate($request, $this)
+ ->authorizedUpdate($request);
+ }
+
+ private function storeFields(RestifyRequest $request)
+ {
+ return $this->collectFields($request)
+ ->forStore($request, $this)
+ ->authorizedStore($request);
}
/**
@@ -147,14 +216,13 @@ public function withResource($resource)
/**
* Resolve repository with given model.
+ *
* @param $model
* @return Repository
*/
public static function resolveWith($model)
{
- /**
- * @var Repository
- */
+ /** * @var Repository $self */
$self = resolve(static::class);
return $self->withResource($model);
@@ -206,42 +274,353 @@ public static function routes(Router $router, $attributes, $wrap = false)
}
/**
- * Resolve the resource to an array.
+ * Return the attributes list.
*
- * @param \Illuminate\Http\Request|null $request
+ * Resolve all model fields through showCallback methods and exclude from the final response if
+ * that is required by method
+ *
+ * @param $request
* @return array
*/
- public function resolve($request = null)
+ public function resolveShowAttributes(RestifyRequest $request)
{
- $data = $this->toArray(
- $request = $request ?: Container::getInstance()->make(RestifyRequest::class)
- );
+ $fields = $this->showFields($request)
+ ->filter(fn (Field $field) => $field->authorize($request))
+ ->each(fn (Field $field) => $field->resolveForShow($this))
+ ->map(fn (Field $field) => $field->serializeToValue($request))
+ ->mapWithKeys(fn ($value) => $value)
+ ->all();
+
+ if ($this instanceof Mergeable) {
+ // Hiden and authorized index fields
+ $fields = $this->modelAttributes($request)
+ ->filter(function ($value, $attribute) use ($request) {
+ /** * @var Field $field */
+ $field = $this->collectFields($request)->firstWhere('attribute', $attribute);
+
+ if (is_null($field)) {
+ return true;
+ }
+
+ if ($field->isHiddenOnShow($request, $this)) {
+ return false;
+ }
+
+ if (! $field->authorize($request)) {
+ return false;
+ }
- if ($data instanceof Arrayable) {
- $data = $data->toArray();
- } elseif ($data instanceof JsonSerializable) {
- $data = $data->jsonSerialize();
+ return true;
+ })->all();
}
- return $this->filter((array) $data);
+ return $fields;
}
/**
- * @return array|mixed
+ * Return the attributes list.
+ *
+ * @param RestifyRequest $request
+ * @return array
*/
- public function jsonSerialize()
+ public function resolveIndexAttributes($request)
+ {
+ // Resolve the show method, and attach the value to the array
+ $fields = $this->indexFields($request)
+ ->filter(fn (Field $field) => $field->authorize($request))
+ ->each(fn (Field $field) => $field->resolveForIndex($this))
+ ->map(fn (Field $field) => $field->serializeToValue($request))
+ ->mapWithKeys(fn ($value) => $value)
+ ->all();
+
+ if ($this instanceof Mergeable) {
+ // Hiden and authorized index fields
+ $fields = $this->modelAttributes($request)
+ ->filter(function ($value, $attribute) use ($request) {
+ /** * @var Field $field */
+ $field = $this->collectFields($request)->firstWhere('attribute', $attribute);
+
+ if (is_null($field)) {
+ return true;
+ }
+
+ if ($field->isHiddenOnIndex($request, $this)) {
+ return false;
+ }
+
+ if (! $field->authorize($request)) {
+ return false;
+ }
+
+ return true;
+ })->all();
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @param $request
+ * @return array
+ */
+ public function resolveDetailsMeta($request)
+ {
+ return [
+ 'authorizedToShow' => $this->authorizedToShow($request),
+ 'authorizedToStore' => $this->authorizedToStore($request),
+ 'authorizedToUpdate' => $this->authorizedToUpdate($request),
+ 'authorizedToDelete' => $this->authorizedToDelete($request),
+ ];
+ }
+
+ /**
+ * Return a list with relationship for the current model.
+ *
+ * @param $request
+ * @return array
+ */
+ public function resolveRelationships($request): array
{
- return $this->resolve();
+ if (is_null($request->get('related'))) {
+ return [];
+ }
+
+ $withs = [];
+
+ with(explode(',', $request->get('related')), function ($relations) use ($request, &$withs) {
+ foreach ($relations as $relation) {
+ if (in_array($relation, static::getRelated())) {
+ // @todo check if the resource has the relation
+ /** * @var AbstractPaginator $paginator */
+ $paginator = $this->resource->{$relation}()->paginate($request->get('relatablePerPage') ?? (static::$defaultRelatablePerPage ?? RestifySearchable::DEFAULT_RELATABLE_PER_PAGE));
+
+ $withs[$relation] = $paginator->getCollection()->map(fn (Model $item) => [
+ 'attributes' => $item->toArray(),
+ ]);
+ }
+ }
+ });
+
+ return $withs;
}
/**
- * @param string $content
- * @param int $status
- * @param array $headers
- * @return RestResponse
+ * @param $request
+ * @return array
*/
- public function response($content = '', $status = 200, array $headers = [])
+ public function resolveIndexMeta($request)
+ {
+ return $this->resolveDetailsMeta($request);
+ }
+
+ /**
+ * Return a list with relationship for the current model.
+ *
+ * @param $request
+ * @return array
+ */
+ public function resolveIndexRelationships($request)
+ {
+ return $this->resolveRelationships($request);
+ }
+
+ public function index(RestifyRequest $request)
+ {
+ // Check if the user has the policy allowRestify
+
+ // Check if the model was set under the repository
+ throw_if($this->model() instanceof NullModel, InstanceOfException::because(__('Model is not defined in the repository.')));
+
+ /** *
+ * Apply all of the query: search, match, sort, related.
+ * @var AbstractPaginator $paginator
+ */
+ $paginator = RepositorySearchService::instance()->search($request, $this)->tap(function ($query) use ($request) {
+ // Call the local definition of the query
+ static::indexQuery($request, $query);
+ })->paginate($request->perPage ?? (static::$defaultPerPage ?? RestifySearchable::DEFAULT_PER_PAGE));
+
+ $items = $paginator->getCollection()->map(function ($value) {
+ return static::resolveWith($value);
+ })->filter(function (self $repository) use ($request) {
+ return $repository->authorizedToShow($request);
+ })->values()->map(fn (self $repository) => $repository->serializeForIndex($request));
+
+ return $this->response([
+ 'meta' => RepositoryCollection::meta($paginator->toArray()),
+ 'links' => RepositoryCollection::paginationLinks($paginator->toArray()),
+ 'data' => $items,
+ ]);
+ }
+
+ public function show(RestifyRequest $request, $repositoryId)
+ {
+ return $this->response()->data($this->serializeForShow($request));
+ }
+
+ public function store(RestifyRequest $request)
+ {
+ DB::transaction(function () use ($request) {
+ static::fillFields(
+ $request, $this->resource, $this->storeFields($request)
+ );
+
+ $this->resource->save();
+
+ $this->storeFields($request)->each(fn (Field $field) => $field->invokeAfter($request, $this->resource));
+ });
+
+ static::stored($this->resource, $request);
+
+ return $this->response()
+ ->created()
+ ->model($this->resource)
+ ->header('Location', static::uriTo($this->resource));
+ }
+
+ public function update(RestifyRequest $request, $repositoryId)
+ {
+ $this->resource = DB::transaction(function () use ($request) {
+ $fields = $this->updateFields($request);
+
+ static::fillFields($request, $this->resource, $fields);
+
+ $this->resource->save();
+
+ return $this->resource;
+ });
+
+ return $this->response()
+ ->data($this->serializeForShow($request))
+ ->success();
+ }
+
+ public function destroy(RestifyRequest $request, $repositoryId)
+ {
+ $status = DB::transaction(function () {
+ return $this->resource->delete();
+ });
+
+ static::deleted($status, $request);
+
+ return $this->response()->deleted();
+ }
+
+ public function allowToUpdate(RestifyRequest $request, $payload = null): self
+ {
+ $this->authorizeToUpdate($request);
+
+ $validator = static::validatorForUpdate($request, $this, $payload);
+
+ $validator->validate();
+
+ return $this;
+ }
+
+ public function allowToStore(RestifyRequest $request, $payload = null): self
+ {
+ static::authorizeToStore($request);
+
+ $validator = static::validatorForStoring($request, $payload);
+
+ $validator->validate();
+
+ return $this;
+ }
+
+ public function allowToDestroy(RestifyRequest $request)
+ {
+ $this->authorizeToDelete($request);
+
+ return $this;
+ }
+
+ public function allowToShow($request): self
+ {
+ $this->authorizeToShow($request);
+
+ return $this;
+ }
+
+ public static function stored($repository, $request)
+ {
+ //
+ }
+
+ public static function updated($model, $request)
+ {
+ //
+ }
+
+ public static function deleted($status, $request)
+ {
+ //
+ }
+
+ public function response($content = '', $status = 200, array $headers = []): RestResponse
{
return new RestResponse($content, $status, $headers);
}
+
+ public function serializeForShow(RestifyRequest $request): array
+ {
+ return $this->filter([
+ 'id' => $this->when($this->resource->id, fn () => $this->getShowId($request)),
+ 'type' => $this->when($type = $this->getType($request), $type),
+ 'attributes' => $request->isShowRequest() ? $this->resolveShowAttributes($request) : $this->resolveIndexAttributes($request),
+ 'relationships' => $this->when(value($related = $this->resolveRelationships($request)), $related),
+ 'meta' => $this->when(value($meta = $request->isShowRequest() ? $this->resolveDetailsMeta($request) : $this->resolveIndexMeta($request)), $meta),
+ ]);
+ }
+
+ public function serializeForIndex(RestifyRequest $request): array
+ {
+ return $this->filter([
+ 'id' => $this->when($id = $this->getShowId($request), $id),
+ 'type' => $this->when($type = $this->getType($request), $type),
+ 'attributes' => $this->when((bool) $attrs = $this->resolveIndexAttributes($request), $attrs),
+ 'relationships' => $this->when(value($related = $this->resolveRelationships($request)), $related),
+ 'meta' => $this->when(value($meta = $this->resolveIndexMeta($request)), $meta),
+ ]);
+ }
+
+ protected function getType(RestifyRequest $request): ?string
+ {
+ return $this->model()->getTable();
+ }
+
+ protected function getShowId(RestifyRequest $request): ?string
+ {
+ return $this->resource->getKey();
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serializeForShow(app(RestifyRequest::class));
+ }
+
+ private function modelAttributes(Request $request = null): Collection
+ {
+ return collect(method_exists($this->resource, 'toArray') ? $this->resource->toArray() : []);
+ }
+
+ /**
+ * Fill each field separately.
+ *
+ * @param RestifyRequest $request
+ * @param Model $model
+ * @param Collection $fields
+ * @return Collection
+ */
+ protected static function fillFields(RestifyRequest $request, Model $model, Collection $fields)
+ {
+ return $fields->map(function (Field $field) use ($request, $model) {
+ return $field->fillAttribute($request, $model);
+ });
+ }
+
+ public static function uriTo(Model $model)
+ {
+ return Restify::path().'/'.static::uriKey().'/'.$model->getKey();
+ }
}
diff --git a/src/Repositories/RepositoryCollection.php b/src/Repositories/RepositoryCollection.php
index 5353f9804..e1edf8d37 100644
--- a/src/Repositories/RepositoryCollection.php
+++ b/src/Repositories/RepositoryCollection.php
@@ -4,9 +4,6 @@
use Illuminate\Support\Arr;
-/**
- * @author Eduard Lupacescu
- */
class RepositoryCollection
{
/**
diff --git a/src/Repositories/RepositoryFillFields.php b/src/Repositories/RepositoryFillFields.php
deleted file mode 100644
index 6873cbda2..000000000
--- a/src/Repositories/RepositoryFillFields.php
+++ /dev/null
@@ -1,87 +0,0 @@
-
- */
-trait RepositoryFillFields
-{
- /**
- * Fill fields on store request.
- *
- * @param RestifyRequest $request
- * @param $model
- * @return array
- */
- public static function fillWhenStore(RestifyRequest $request, $model)
- {
- static::fillFields(
- $request, $model,
- static::resolveWith($model)->collectFields($request)
- );
-
- static::fillExtra($request, $model,
- static::resolveWith($model)->collectFields($request)
- );
-
- return $model;
- }
-
- /**
- * @param RestifyRequest $request
- * @param $model
- * @return array
- */
- public static function fillWhenUpdate(RestifyRequest $request, $model)
- {
- $fields = static::resolveWith($model)->collectFields($request);
-
- static::fillFields($request, $model, $fields);
- static::fillExtra($request, $model, $fields);
-
- return $model;
- }
-
- /**
- * Fill each field separately.
- *
- * @param RestifyRequest $request
- * @param Model $model
- * @param Collection $fields
- * @return Model
- */
- protected static function fillFields(RestifyRequest $request, Model $model, Collection $fields)
- {
- $fields->map(function (Field $field) use ($request, $model) {
- return $field->fillAttribute($request, $model);
- })->values()->all();
-
- return $model;
- }
-
- /**
- * If some fields were not defined in the @fields method, but they are in fillable attributes and present in request,
- * they should be also filled on request.
- * @param RestifyRequest $request
- * @param Model $model
- * @param Collection $fields
- * @return array
- */
- protected static function fillExtra(RestifyRequest $request, Model $model, Collection $fields)
- {
- $definedAttributes = $fields->map->getAttribute()->toArray();
- $fromRequest = collect($request->only($model->getFillable()))->keys()->filter(function ($attribute) use ($definedAttributes) {
- return ! in_array($attribute, $definedAttributes);
- });
-
- return $fromRequest->each(function ($attribute) use ($request, $model) {
- $model->{$attribute} = $request->{$attribute};
- })->values()->all();
- }
-}
diff --git a/src/Repositories/ResponseResolver.php b/src/Repositories/ResponseResolver.php
deleted file mode 100644
index 8ed74c3b1..000000000
--- a/src/Repositories/ResponseResolver.php
+++ /dev/null
@@ -1,171 +0,0 @@
-
- */
-trait ResponseResolver
-{
- /**
- * Return the attributes list.
- *
- * Resolve all model fields through showCallback methods and exclude from the final response if
- * that is required by method
- *
- * @param $request
- * @return array
- */
- public function resolveDetailsAttributes(RestifyRequest $request)
- {
- $resolvedAttributes = [];
- $modelAttributes = method_exists($this->resource, 'toArray') ? $this->resource->toArray($request) : [];
- $this->collectFields($request)->filter(function (Field $field) {
- return is_callable($field->showCallback);
- })->map(function (Field $field) use (&$resolvedAttributes) {
- $resolvedAttributes[$field->attribute] = $field->resolveForShow($this);
- });
-
- $resolved = array_merge($modelAttributes, $resolvedAttributes);
-
- if ($request->isDetailRequest()) {
- $hidden = $this->collectFields($request)->filter->isHiddenOnDetail($request, $this)->pluck('attribute')->toArray();
-
- $resolved = Arr::except($resolved, $hidden);
- }
-
- if ($request->isIndexRequest()) {
- $hidden = $this->collectFields($request)->filter->isHiddenOnIndex($request, $this)->pluck('attribute')->toArray();
-
- $resolved = Arr::except($resolved, $hidden);
- }
-
- return $resolved;
- }
-
- /**
- * @param $request
- * @return array
- */
- public function resolveDetailsMeta($request)
- {
- return [
- 'authorizedToShow' => $this->authorizedToShow($request),
- 'authorizedToStore' => $this->authorizedToStore($request),
- 'authorizedToUpdate' => $this->authorizedToUpdate($request),
- 'authorizedToDelete' => $this->authorizedToDelete($request),
- ];
- }
-
- /**
- * Return a list with relationship for the current model.
- *
- * @param $request
- * @return array
- */
- public function resolveDetailsRelationships($request)
- {
- if (is_null($request->get('with'))) {
- return [];
- }
-
- $withs = [];
-
- if ($this->resource instanceof RestifySearchable) {
- with(explode(',', $request->get('with')), function ($relations) use ($request, &$withs) {
- foreach ($relations as $relation) {
- if (in_array($relation, $this->resource::getWiths())) {
- /**
- * @var AbstractPaginator
- */
- $paginator = $this->resource->{$relation}()->paginate($request->get('relatablePerPage') ?? ($this->resource::$defaultRelatablePerPage ?? RestifySearchable::DEFAULT_RELATABLE_PER_PAGE));
- /** * @var Builder $q */
- $q = $this->resource->{$relation}->first();
- /** * @var Repository $repository */
- if ($q && $repository = Restify::repositoryForModel($q->getModel())) {
- // This will serialize into the repository dedicated for model
- $relatable = $paginator->getCollection()->map(function ($value) use ($repository) {
- return $repository::resolveWith($value);
- });
- } else {
- // This will fallback into serialization of the parent formatting
- $relatable = $paginator->getCollection()->map(function ($value) use ($repository) {
- return $repository::resolveWith($value);
- });
- }
-
- unset($relatable['meta']);
- unset($relatable['links']);
-
- $withs[$relation] = $relatable;
- }
- }
- });
- }
-
- return $withs;
- }
-
- /**
- * Resolve the response for the details.
- *
- * @param $request
- * @param $serialized
- * @return array
- */
- public function serializeDetails(RestifyRequest $request, $serialized)
- {
- return $serialized;
- }
-
- /**
- * Resolve the response for the index request.
- *
- * @param $request
- * @param $serialized
- * @return array
- */
- public function serializeIndex($request, $serialized)
- {
- return $serialized;
- }
-
- /**
- * Return the attributes list.
- *
- * @param $request
- * @return array
- */
- public function resolveIndexAttributes($request)
- {
- return $this->resolveDetailsAttributes($request);
- }
-
- /**
- * @param $request
- * @return array
- */
- public function resolveIndexMeta($request)
- {
- return $this->resolveDetailsMeta($request);
- }
-
- /**
- * Return a list with relationship for the current model.
- *
- * @param $request
- * @return array
- */
- public function resolveIndexRelationships($request)
- {
- return $this->resolveDetailsRelationships($request);
- }
-}
diff --git a/src/RestifyCustomRoutesProvider.php b/src/RestifyCustomRoutesProvider.php
index e654e2d38..fac945777 100644
--- a/src/RestifyCustomRoutesProvider.php
+++ b/src/RestifyCustomRoutesProvider.php
@@ -12,19 +12,11 @@
*/
class RestifyCustomRoutesProvider extends ServiceProvider
{
- /**
- * Bootstrap the application services.
- */
public function boot()
{
$this->registerRoutes();
}
- /**
- * Register the package routes.
- *
- * @return void
- */
protected function registerRoutes()
{
collect(Restify::$repositories)->each(function ($repository) {
diff --git a/src/Services/Search/RepositorySearchService.php b/src/Services/Search/RepositorySearchService.php
new file mode 100644
index 000000000..b69deb0ff
--- /dev/null
+++ b/src/Services/Search/RepositorySearchService.php
@@ -0,0 +1,173 @@
+repository = $repository;
+
+ $query = $this->prepareMatchFields($request, $this->prepareSearchFields($request, $repository::query(), $this->fixedInput), $this->fixedInput);
+
+ return $this->prepareRelations($request, $this->prepareOrders($request, $query), $this->fixedInput);
+ }
+
+ public function prepareMatchFields(RestifyRequest $request, $query, $extra = [])
+ {
+ $model = $query->getModel();
+ foreach ($this->repository->getMatchByFields() as $key => $type) {
+ if (! $request->has($key) && ! data_get($extra, "match.$key")) {
+ continue;
+ }
+
+ $value = $request->get($key, data_get($extra, "match.$key"));
+
+ if (empty($value)) {
+ continue;
+ }
+
+ $field = $model->qualifyColumn($key);
+
+ $values = explode(',', $value);
+
+ foreach ($values as $match) {
+ switch ($this->repository->getMatchByFields()[$key]) {
+ case RestifySearchable::MATCH_TEXT:
+ case 'string':
+ $query->where($field, '=', $match);
+ break;
+ case RestifySearchable::MATCH_BOOL:
+ case 'boolean':
+ if ($match === 'false') {
+ $query->where(function ($query) use ($field) {
+ return $query->where($field, '=', false)->orWhereNull($field);
+ });
+ break;
+ }
+ $query->where($field, '=', true);
+ break;
+ case RestifySearchable::MATCH_INTEGER:
+ case 'number':
+ case 'int':
+ $query->where($field, '=', (int) $match);
+ break;
+ }
+ }
+ }
+
+ return $query;
+ }
+
+ public function prepareOrders(RestifyRequest $request, $query, $extra = []): Builder
+ {
+ $sort = $request->get('sort', '');
+
+ if (isset($extra['sort'])) {
+ $sort = $extra['sort'];
+ }
+
+ $params = explode(',', $sort);
+
+ if (is_array($params) === true && empty($params) === false) {
+ foreach ($params as $param) {
+ $this->setOrder($query, $param);
+ }
+ }
+
+ if (empty($params) === true) {
+ $this->setOrder($query, '+id');
+ }
+
+ return $query;
+ }
+
+ public function prepareRelations(RestifyRequest $request, $query, $extra = []): Builder
+ {
+ $relations = array_merge($extra, explode(',', $request->get('with')));
+
+ foreach ($relations as $relation) {
+ if (in_array($relation, $this->repository->getWiths())) {
+ $query->with($relation);
+ }
+ }
+
+ return $query;
+ }
+
+ public function prepareSearchFields(RestifyRequest $request, $query, $extra = []): Builder
+ {
+ $search = $request->get('search', data_get($extra, 'search', ''));
+ $model = $query->getModel();
+
+ $query->where(function (Builder $query) use ($search, $model) {
+ $connectionType = $model->getConnection()->getDriverName();
+
+ $canSearchPrimaryKey = is_numeric($search) &&
+ in_array($query->getModel()->getKeyType(), ['int', 'integer']) &&
+ ($connectionType != 'pgsql' || $search <= PHP_INT_MAX) &&
+ in_array($query->getModel()->getKeyName(), $model::getSearchableFields());
+
+ if ($canSearchPrimaryKey) {
+ $query->orWhere($query->getModel()->getQualifiedKeyName(), $search);
+ }
+
+ $likeOperator = $connectionType == 'pgsql' ? 'ilike' : 'like';
+
+ foreach ($this->repository->getSearchableFields() as $column) {
+ $query->orWhere($model->qualifyColumn($column), $likeOperator, '%'.$search.'%');
+ }
+ });
+
+ return $query;
+ }
+
+ public function setOrder($query, $param)
+ {
+ if ($param === 'random') {
+ $query->inRandomOrder();
+
+ return $query;
+ }
+
+ $order = substr($param, 0, 1);
+
+ if ($order === '-') {
+ $field = substr($param, 1);
+ }
+
+ if ($order === '+') {
+ $field = substr($param, 1);
+ }
+
+ if ($order !== '-' && $order !== '+') {
+ $order = '+';
+ $field = $param;
+ }
+
+ if (isset($field)) {
+ if (in_array($field, $this->repository->getOrderByFields()) === true) {
+ if ($order === '-') {
+ $query->orderBy($field, 'desc');
+ }
+
+ if ($order === '+') {
+ $query->orderBy($field, 'asc');
+ }
+ }
+
+ if ($field === 'random') {
+ $query->orderByRaw('RAND()');
+ }
+ }
+
+ return $query;
+ }
+}
diff --git a/src/Services/Search/SearchService.php b/src/Services/Search/SearchService.php
index d25f39c25..025e581a0 100644
--- a/src/Services/Search/SearchService.php
+++ b/src/Services/Search/SearchService.php
@@ -3,7 +3,6 @@
namespace Binaryk\LaravelRestify\Services\Search;
use Binaryk\LaravelRestify\Contracts\RestifySearchable;
-use Binaryk\LaravelRestify\Exceptions\InstanceOfException;
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
use Illuminate\Database\Eloquent\Builder;
@@ -14,13 +13,6 @@
*/
class SearchService extends Searchable
{
- /**
- * @param RestifyRequest $request
- * @param $model
- * @return Builder
- * @throws InstanceOfException
- * @throws \Throwable
- */
public function search(RestifyRequest $request, $model)
{
if (! $model instanceof RestifySearchable) {
@@ -35,9 +27,9 @@ public function search(RestifyRequest $request, $model)
/**
* Prepare eloquent exact fields.
*
- * @param RestifyRequest $request
- * @param Builder $query
- * @param array $extra
+ * @param RestifyRequest $request
+ * @param Builder $query
+ * @param array $extra
* @return Builder
*/
public function prepareMatchFields(RestifyRequest $request, $query, $extra = [])
@@ -91,9 +83,9 @@ public function prepareMatchFields(RestifyRequest $request, $query, $extra = [])
/**
* Prepare eloquent order by.
*
- * @param RestifyRequest $request
+ * @param RestifyRequest $request
* @param $query
- * @param array $extra
+ * @param array $extra
* @return Builder
*/
public function prepareOrders(RestifyRequest $request, $query, $extra = [])
@@ -122,9 +114,9 @@ public function prepareOrders(RestifyRequest $request, $query, $extra = [])
/**
* Prepare relations.
*
- * @param RestifyRequest $request
- * @param Builder $query
- * @param array $extra
+ * @param RestifyRequest $request
+ * @param Builder $query
+ * @param array $extra
* @return Builder
*/
public function prepareRelations(RestifyRequest $request, $query, $extra = [])
@@ -145,9 +137,9 @@ public function prepareRelations(RestifyRequest $request, $query, $extra = [])
/**
* Prepare search.
*
- * @param RestifyRequest $request
- * @param Builder $query
- * @param array $extra
+ * @param RestifyRequest $request
+ * @param Builder $query
+ * @param array $extra
* @return Builder
*/
public function prepareSearchFields(RestifyRequest $request, $query, $extra = [])
diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php
index 7f70a93b7..510fb5783 100644
--- a/src/Traits/AuthorizableModels.php
+++ b/src/Traits/AuthorizableModels.php
@@ -5,26 +5,17 @@
use Binaryk\LaravelRestify\Repositories\Repository;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
/**
* Could be used as a trait in a model class and in a repository class.
*
- * @property Model resource
+ * @property Model $resource
* @author Eduard Lupacescu
*/
trait AuthorizableModels
{
- /**
- * @return static
- */
- public static function newModel()
- {
- return new static;
- }
-
/**
* Determine if the given resource is authorizable.
*
@@ -36,20 +27,20 @@ public static function authorizable()
}
/**
- * Determine if the resource should be available for the given request.
+ * Determine if the Restify is enabled for this repository.
*
* @param \Illuminate\Http\Request $request
* @return void
* @throws AuthorizationException
*/
- public function authorizeToShowAny(Request $request)
+ public function authorizeToUseRepository(Request $request)
{
if (! static::authorizable()) {
return;
}
- if (method_exists(Gate::getPolicyFor(static::newModel()), 'showAny')) {
- $this->authorizeTo($request, 'showAny');
+ if (method_exists(Gate::getPolicyFor(static::newModel()), 'allowRestify')) {
+ $this->authorizeTo($request, 'allowRestify');
}
}
@@ -59,49 +50,14 @@ public function authorizeToShowAny(Request $request)
* @param \Illuminate\Http\Request $request
* @return bool
*/
- public static function authorizedToShowAny(Request $request)
+ public static function authorizedToUseRepository(Request $request)
{
if (! static::authorizable()) {
return true;
}
- return method_exists(Gate::getPolicyFor(static::newModel()), 'showAny')
- ? Gate::check('showAny', get_class(static::newModel()))
- : true;
- }
-
- /**
- * Determine if the resource should be available for the given request (.
- *
- * @param \Illuminate\Http\Request $request
- * @return void
- * @throws AuthorizationException
- */
- public function authorizeToShowEvery(Request $request)
- {
- if (! static::authorizable()) {
- return;
- }
-
- if (method_exists(Gate::getPolicyFor(static::newModel()), 'showEvery')) {
- $this->authorizeTo($request, 'showEvery');
- }
- }
-
- /**
- * Determine if the resource should be available for the given request.
- *
- * @param \Illuminate\Http\Request $request
- * @return bool
- */
- public static function authorizedToShowEvery(Request $request)
- {
- if (! static::authorizable()) {
- return true;
- }
-
- return method_exists(Gate::getPolicyFor(static::newModel()), 'showEvery')
- ? Gate::check('showEvery', get_class(static::newModel()))
+ return method_exists(Gate::getPolicyFor(static::newModel()), 'allowRestify')
+ ? Gate::check('allowRestify', get_class(static::newModel()))
: true;
}
@@ -230,25 +186,7 @@ public function authorizeTo(Request $request, $ability)
*/
public function authorizedTo(Request $request, $ability)
{
- return static::authorizable() ? Gate::check($ability, $this->determineModel()) : true;
- }
-
- /**
- * Since this trait could be used by a repository or by a model, we have to
- * detect the model from either class.
- *
- * @return AuthorizableModels|Model|mixed|null
- * @throws ModelNotFoundException
- */
- public function determineModel()
- {
- $model = $this->isRepositoryContext() === false ? $this : ($this->resource ?? null);
-
- if (is_null($model)) {
- throw new ModelNotFoundException(__('Model is not declared in :class', ['class' => self::class]));
- }
-
- return $model;
+ return static::authorizable() ? Gate::check($ability, $this->resource) : true;
}
/**
diff --git a/src/Traits/InteractWithSearch.php b/src/Traits/InteractWithSearch.php
index 54327b6bc..a4344b3d7 100644
--- a/src/Traits/InteractWithSearch.php
+++ b/src/Traits/InteractWithSearch.php
@@ -10,14 +10,14 @@ trait InteractWithSearch
use AuthorizableModels;
public static $defaultPerPage = 15;
+
public static $defaultRelatablePerPage = 15;
- /**
- * @return array
- */
public static function getSearchableFields()
{
- return static::$search ?? [];
+ return empty(static::$search)
+ ? [static::newModel()->getKeyName()]
+ : static::$search;
}
/**
@@ -28,12 +28,22 @@ public static function getWiths()
return static::$withs ?? [];
}
+ /**
+ * @return array
+ */
+ public static function getRelated()
+ {
+ return static::$related ?? [];
+ }
+
/**
* @return array
*/
public static function getMatchByFields()
{
- return static::$match ?? [];
+ return empty(static::$match)
+ ? [static::newModel()->getKeyName()]
+ : static::$match;
}
/**
@@ -41,6 +51,8 @@ public static function getMatchByFields()
*/
public static function getOrderByFields()
{
- return static::$sort ?? [];
+ return empty(static::$sort)
+ ? [static::newModel()->getKeyName()]
+ : static::$sort;
}
}
diff --git a/src/Traits/Make.php b/src/Traits/Make.php
new file mode 100644
index 000000000..9252c65c5
--- /dev/null
+++ b/src/Traits/Make.php
@@ -0,0 +1,16 @@
+
+ */
+class IndexControllerTest extends IntegrationTest
+{
+ public function test_list_repository()
+ {
+ factory(User::class)->create();
+ factory(User::class)->create();
+ factory(User::class)->create();
+
+ $response = $this->withExceptionHandling()
+ ->getJson('/restify-api/users');
+
+ $response->assertJsonCount(3, 'data');
+ }
+
+ public function test_the_rest_controller_can_paginate()
+ {
+ $this->mockUsers(20);
+
+ $class = (new class extends RestController {
+ public function users()
+ {
+ return $this->response($this->search(User::class));
+ }
+ });
+
+ $response = $class->search(User::class, [
+ 'match' => [
+ 'id' => 1,
+ ],
+ ]);
+ $this->assertIsArray($class->search(User::class));
+ $this->assertCount(1, $response['data']);
+ $this->assertEquals(count($class->users()->getData()->data), User::$defaultPerPage);
+ }
+}
diff --git a/tests/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php
index 1ba8e0574..95839f70c 100644
--- a/tests/Controllers/RepositoryIndexControllerTest.php
+++ b/tests/Controllers/RepositoryIndexControllerTest.php
@@ -3,230 +3,181 @@
namespace Binaryk\LaravelRestify\Tests\Controllers;
use Binaryk\LaravelRestify\Contracts\RestifySearchable;
-use Binaryk\LaravelRestify\Controllers\RestController;
+use Binaryk\LaravelRestify\Fields\Field;
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
+use Binaryk\LaravelRestify\Repositories\Mergeable;
+use Binaryk\LaravelRestify\Repositories\Repository;
use Binaryk\LaravelRestify\Restify;
-use Binaryk\LaravelRestify\Tests\Fixtures\User;
+use Binaryk\LaravelRestify\Tests\Fixtures\Apple;
+use Binaryk\LaravelRestify\Tests\Fixtures\AppleRepository;
use Binaryk\LaravelRestify\Tests\IntegrationTest;
-use Mockery;
+use Illuminate\Foundation\Testing\RefreshDatabase;
-/**
- * @author Eduard Lupacescu
- */
class RepositoryIndexControllerTest extends IntegrationTest
{
- public function test_list_resource()
+ use RefreshDatabase;
+
+ public function test_repository_per_page()
{
- factory(User::class)->create();
- factory(User::class)->create();
- factory(User::class)->create();
+ factory(Apple::class, 20)->create();
+
+ AppleRepository::$defaultPerPage = 5;
+
+ $response = $this->getJson('restify-api/apples')
+ ->assertStatus(200);
- $response = $this->withExceptionHandling()
- ->getJson('/restify-api/users');
+ $this->assertCount(5, $response->json('data'));
- $response->assertJsonCount(3, 'data');
+ $response = $this->getJson('restify-api/apples?perPage=10');
+
+ $this->assertCount(10, $response->json('data'));
}
- public function test_the_rest_controller_can_paginate()
+ public function test_repository_search_query_works()
{
- $this->mockUsers(20);
-
- $class = (new class extends RestController {
- public function users()
- {
- return $this->response($this->search(User::class));
- }
- });
-
- $response = $class->search(User::class, [
- 'match' => [
- 'id' => 1,
- ],
+ factory(Apple::class)->create([
+ 'title' => 'Some title',
+ ]);
+
+ factory(Apple::class)->create([
+ 'title' => 'Another one',
]);
- $this->assertIsArray($class->search(User::class));
- $this->assertCount(1, $response['data']);
- $this->assertEquals(count($class->users()->getData()->data), User::$defaultPerPage);
+
+ factory(Apple::class)->create([
+ 'title' => 'foo another',
+ ]);
+
+ factory(Apple::class)->create([
+ 'title' => 'Third apple',
+ ]);
+
+ AppleRepository::$search = ['title'];
+
+ $response = $this->getJson('restify-api/apples?search=another')
+ ->assertStatus(200);
+
+ $this->assertCount(2, $response->json('data'));
}
- public function test_that_default_per_page_works()
+ public function test_repository_filter_works()
{
- User::$defaultPerPage = 40;
- $this->mockUsers(50);
-
- $class = (new class extends RestController {
- public function users()
- {
- return $this->response($this->search(User::class));
- }
- });
-
- $response = $class->search(User::class, [
- 'match' => [
- 'id' => 1,
- ],
+ AppleRepository::$match = [
+ 'title' => RestifySearchable::MATCH_TEXT,
+ ];
+
+ factory(Apple::class)->create([
+ 'title' => 'Some title',
+ ]);
+
+ factory(Apple::class)->create([
+ 'title' => 'Another one',
]);
- $this->assertIsArray($class->search(User::class));
- $this->assertCount(1, $response['data']);
- $this->assertEquals(count($class->users()->getData()->data), 40);
- User::$defaultPerPage = RestifySearchable::DEFAULT_PER_PAGE;
+
+ $response = $this
+ ->getJson('restify-api/apples?title=Another one')
+ ->assertStatus(200);
+
+ $this->assertCount(1, $response->json('data'));
}
- public function test_search_query_works()
+ public function test_repository_order()
{
- $users = $this->mockUsers(10, ['eduard.lupacescu@binarcode.com']);
- $model = $users->where('email', 'eduard.lupacescu@binarcode.com')->first(); //find manually the model
- $repository = Restify::repositoryForModel(get_class($model));
- $expected = $repository::resolveWith($model)->toArray(resolve(RestifyRequest::class));
- unset($expected['relationships']);
-
- $r = $this->withExceptionHandling()
- ->getJson('/restify-api/users?search=eduard.lupacescu@binarcode.com')
- ->assertStatus(200)
- ->assertJsonStructure([
- 'links' => [
- 'last',
- 'next',
- 'first',
- 'prev',
- ],
- 'meta' => [
- 'path',
- 'current_page',
- 'from',
- 'last_page',
- 'per_page',
- 'to',
- 'total',
- ],
- 'data',
- ])->decodeResponseJson();
-
- $this->assertCount(1, $r['data']);
-
- $this->withExceptionHandling()
- ->getJson('/restify-api/users?search=some_unexpected_string_here')
- ->assertStatus(200)
- ->assertJson([
- 'links' => [
- 'next' => null,
- 'last' => 'http://localhost/restify-api/users?page=1',
- 'first' => 'http://localhost/restify-api/users?page=1',
- 'prev' => null,
- ],
- 'meta' => [
- 'current_page' => 1,
- 'from' => null,
- 'last_page' => 1,
- 'per_page' => 15,
- 'to' => null,
- 'path' => 'http://localhost/restify-api/users',
- 'total' => 0,
- ],
- 'data' => [],
- ]);
+ AppleRepository::$sort = [
+ 'title',
+ ];
+
+ factory(Apple::class)->create(['title' => 'aaa']);
+
+ factory(Apple::class)->create(['title' => 'zzz']);
+
+ $response = $this
+ ->getJson('restify-api/apples?sort=-title')
+ ->assertStatus(200);
+
+ $this->assertEquals('zzz', $response->json('data.0.attributes.title'));
+ $this->assertEquals('aaa', $response->json('data.1.attributes.title'));
+
+ $response = $this
+ ->getJson('restify-api/apples?order=-title')
+ ->assertStatus(200);
+
+ $this->assertEquals('zzz', $response->json('data.1.attributes.title'));
+ $this->assertEquals('aaa', $response->json('data.0.attributes.title'));
}
- public function test_that_desc_sort_query_param_works()
+ public function test_repsitory_with_relations()
{
- $this->mockUsers(10);
- $response = $this->withExceptionHandling()->get('/restify-api/users?sort=-id')
- ->assertStatus(200)
- ->getOriginalContent();
+ AppleRepository::$related = ['user'];
+
+ $user = $this->mockUsers(1)->first();
+
+ factory(Apple::class)->create(['user_id' => $user->id]);
- $this->assertSame($response['data']->first()->resource->id, 10);
- $this->assertSame($response['data']->last()->resource->id, 1);
+ $response = $this->getJson('/restify-api/apples?related=user')
+ ->assertStatus(200);
+
+ $this->assertCount(1, $response->json('data.0.relationships.user'));
+ $this->assertArrayNotHasKey('user', $response->json('data.0.attributes'));
}
- public function test_that_asc_sort_query_param_works()
+ public function test_index_unmergeable_repository_containes_only_explicitly_defined_fields()
{
- $this->mockUsers(10);
+ Restify::repositories([
+ AppleTitleRepository::class,
+ ]);
+
+ factory(Apple::class)->create();
- $response = (array) json_decode($this->withExceptionHandling()->get('/restify-api/users?sort=+id')
- ->assertStatus(200)
- ->getContent());
+ $response = $this->get('/restify-api/apples-title')
+ ->assertStatus(200);
- $this->assertSame(data_get($response, 'data.0.id'), 1);
- $this->assertSame(data_get($response, 'data.9.id'), 10);
+ $this->assertArrayHasKey('title', $response->json('data.0.attributes'));
+
+ $this->assertArrayNotHasKey('id', $response->json('data.0.attributes'));
+ $this->assertArrayNotHasKey('created_at', $response->json('data.0.attributes'));
}
- public function test_that_default_asc_sort_query_param_works()
+ public function test_index_mergeable_repository_containes_model_attributes_and_local_fields()
{
- $this->mockUsers(10);
+ Restify::repositories([
+ AppleMergeable::class,
+ ]);
- $response = (array) json_decode($this->withExceptionHandling()->get('/restify-api/users?sort=id')
- ->assertStatus(200)
- ->getContent());
+ factory(Apple::class)->create();
- $this->assertSame(data_get($response, 'data.0.id'), 1);
- $this->assertSame(data_get($response, 'data.9.id'), 10);
+ $response = $this->get('/restify-api/apples-title-mergeable')
+ ->assertStatus(200);
+
+ $this->assertArrayHasKey('title', $response->json('data.0.attributes'));
+ $this->assertArrayHasKey('id', $response->json('data.0.attributes'));
+ $this->assertArrayHasKey('created_at', $response->json('data.0.attributes'));
}
+}
+
+class AppleTitleRepository extends Repository
+{
+ public static $uriKey = 'apples-title';
+
+ public static $model = Apple::class;
- public function test_that_match_param_works()
+ public function fields(RestifyRequest $request)
{
- User::$match = ['email' => RestifySearchable::MATCH_TEXT]; // it will automatically filter over these queries (email='test@email.com')
- $users = $this->mockUsers(10, ['eduard.lupacescu@binarcode.com']);
- $request = Mockery::mock(RestifyRequest::class);
- $request->shouldReceive('has')
- ->andReturnFalse();
- $request->shouldReceive('get')
- ->andReturnFalse();
- $request->shouldReceive('isDetailRequest')
- ->andReturnFalse();
- $request->shouldReceive('isIndexRequest')
- ->andReturnTrue();
-
- $model = $users->where('email', 'eduard.lupacescu@binarcode.com')->first();
- $repository = Restify::repositoryForModel(get_class($model));
- $expected = $repository::resolveWith($model)->toArray($request);
-
- unset($expected['relationships']);
- $this->withExceptionHandling()
- ->get('/restify-api/users?email=eduard.lupacescu@binarcode.com')
- ->assertStatus(200)
- ->assertJson([
- 'links' => [
- 'last' => 'http://localhost/restify-api/users?page=1',
- 'next' => null,
- 'first' => 'http://localhost/restify-api/users?page=1',
- 'prev' => null,
- ],
- 'meta' => [
- 'current_page' => 1,
- 'path' => 'http://localhost/restify-api/users',
- 'from' => 1,
- 'last_page' => 1,
- 'per_page' => 15,
- 'to' => 1,
- 'total' => 1,
- ],
- 'data' => [$expected],
- ]);
+ return [
+ Field::make('title'),
+ ];
}
+}
+
+class AppleMergeable extends Repository implements Mergeable
+{
+ public static $uriKey = 'apples-title-mergeable';
- public function test_that_with_param_works()
+ public static $model = Apple::class;
+
+ public function fields(RestifyRequest $request)
{
- User::$match = ['email' => RestifySearchable::MATCH_TEXT]; // it will automatically filter over these queries (email='test@email.com')
- $this->mockUsers(1);
- $posts = $this->mockPosts(1, 2);
- $request = Mockery::mock(RestifyRequest::class);
- $request->shouldReceive('has')
- ->andReturnTrue();
- $request->shouldReceive('get')
- ->andReturn('posts');
- $request->shouldReceive('isDetailRequest')
- ->andReturnFalse();
- $request->shouldReceive('isIndexRequest')
- ->andReturnTrue();
-
- $r = $this->withExceptionHandling()
- ->getJson('/restify-api/users?with=posts')
- ->assertStatus(200)
- ->getContent();
- $r = (array) json_decode($r);
-
- $this->assertSameSize((array) data_get($r, 'data.0.relationships.posts'), $posts->toArray());
- $this->assertSame(array_keys((array) data_get($r, 'data.0.relationships.posts.0')), [
- 'id', 'type', 'attributes', 'meta',
- ]);
+ return [
+ Field::make('title'),
+ ];
}
}
diff --git a/tests/Controllers/RepositoryShowControllerTest.php b/tests/Controllers/RepositoryShowControllerTest.php
index b1e42dc18..c6a27a044 100644
--- a/tests/Controllers/RepositoryShowControllerTest.php
+++ b/tests/Controllers/RepositoryShowControllerTest.php
@@ -2,6 +2,12 @@
namespace Binaryk\LaravelRestify\Tests\Controllers;
+use Binaryk\LaravelRestify\Fields\Field;
+use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
+use Binaryk\LaravelRestify\Repositories\Mergeable;
+use Binaryk\LaravelRestify\Repositories\Repository;
+use Binaryk\LaravelRestify\Restify;
+use Binaryk\LaravelRestify\Tests\Fixtures\Apple;
use Binaryk\LaravelRestify\Tests\Fixtures\Post;
use Binaryk\LaravelRestify\Tests\IntegrationTest;
@@ -20,7 +26,7 @@ public function test_basic_show()
{
factory(Post::class)->create(['user_id' => 1]);
- $this->withoutExceptionHandling()->get('/restify-api/posts/1')
+ $this->get('/restify-api/posts/1')
->assertStatus(200)
->assertJsonStructure([
'data' => [
@@ -30,4 +36,93 @@ public function test_basic_show()
],
]);
}
+
+ public function test_show_will_authorize_fields()
+ {
+ factory(Apple::class)->create();
+
+ Restify::repositories([
+ AppleAuthorized::class,
+ ]);
+
+ $_SERVER['can.see.title'] = false;
+ $response = $this->getJson('/restify-api/apple-authorized/1');
+
+ $this->assertArrayNotHasKey('title', $response->json('data.attributes'));
+
+ $_SERVER['can.see.title'] = true;
+ $response = $this->getJson('/restify-api/apple-authorized/1');
+
+ $this->assertArrayHasKey('title', $response->json('data.attributes'));
+ }
+
+ public function test_show_will_take_into_consideration_show_callback()
+ {
+ factory(Apple::class)->create([
+ 'title' => 'Eduard',
+ ]);
+
+ Restify::repositories([
+ AppleAuthorized::class,
+ ]);
+
+ $response = $this->getJson('/restify-api/apple-authorized/1');
+
+ $this->assertSame('EDUARD', $response->json('data.attributes.title'));
+ }
+
+ public function test_show_unmergeable_repository_containes_only_explicitly_defined_fields()
+ {
+ factory(Apple::class)->create([
+ 'title' => 'Eduard',
+ ]);
+
+ Restify::repositories([
+ AppleAuthorized::class,
+ ]);
+
+ $response = $this->getJson('/restify-api/apple-authorized/1');
+
+ $this->assertArrayHasKey('title', $response->json('data.attributes'));
+
+ $this->assertArrayNotHasKey('id', $response->json('data.attributes'));
+ $this->assertArrayNotHasKey('created_at', $response->json('data.attributes'));
+ }
+
+ public function test_show_mergeable_repository_containes_model_attributes_and_local_fields()
+ {
+ factory(Apple::class)->create([
+ 'title' => 'Eduard',
+ ]);
+
+ Restify::repositories([
+ AppleAuthorizedMergeable::class,
+ ]);
+
+ $response = $this->getJson('/restify-api/apple-authorized-mergeable/1');
+
+ $this->assertArrayHasKey('title', $response->json('data.attributes'));
+ $this->assertArrayHasKey('id', $response->json('data.attributes'));
+ $this->assertArrayHasKey('created_at', $response->json('data.attributes'));
+ }
+}
+
+class AppleAuthorized extends Repository
+{
+ public static $uriKey = 'apple-authorized';
+
+ public static $model = Apple::class;
+
+ public function fields(RestifyRequest $request)
+ {
+ return [
+ Field::make('title')->canSee(fn () => $_SERVER['can.see.title'] ?? true)
+ ->showCallback(fn ($value) => strtoupper($value)),
+ ];
+ }
+}
+
+class AppleAuthorizedMergeable extends AppleAuthorized implements Mergeable
+{
+ public static $uriKey = 'apple-authorized-mergeable';
}
diff --git a/tests/Controllers/RepositoryStoreControllerTest.php b/tests/Controllers/RepositoryStoreControllerTest.php
index 6d5c60b26..685c97757 100644
--- a/tests/Controllers/RepositoryStoreControllerTest.php
+++ b/tests/Controllers/RepositoryStoreControllerTest.php
@@ -20,15 +20,13 @@ protected function setUp(): void
public function test_basic_validation_works()
{
- $this->withExceptionHandling()->post('/restify-api/posts', [
- 'title' => 'Title',
- ])
+ $this->postJson('/restify-api/posts', [])
->assertStatus(400)
->assertJson([
'errors' => [
[
- 'description' => [
- 'Description field is required',
+ 'title' => [
+ 'This field is required',
],
],
],
@@ -41,7 +39,7 @@ public function test_unauthorized_store()
Gate::policy(Post::class, PostPolicy::class);
- $this->withExceptionHandling()->post('/restify-api/posts', [
+ $this->postJson('/restify-api/posts', [
'title' => 'Title',
'description' => 'Title',
])->assertStatus(403)
@@ -51,19 +49,78 @@ public function test_unauthorized_store()
public function test_success_storing()
{
$user = $this->mockUsers()->first();
- $r = json_decode($this->withoutExceptionHandling()->post('/restify-api/posts', [
+ $r = $this->postJson('/restify-api/posts', [
+ 'user_id' => $user->id,
+ 'title' => 'Some post title',
+ ])->assertStatus(201)
+ ->assertHeader('Location', '/restify-api/posts/1');
+
+ $this->assertEquals('Some post title', $r->json('data.attributes.title'));
+ $this->assertEquals(1, $r->json('data.attributes.user_id'));
+ $this->assertEquals(1, $r->json('data.id'));
+ $this->assertEquals('posts', $r->json('data.type'));
+ }
+
+ public function test_will_store_only_defined_fields_from_fieldsForStore()
+ {
+ $user = $this->mockUsers()->first();
+ $r = $this->postJson('/restify-api/posts', [
+ 'user_id' => $user->id,
+ 'title' => 'Some post title',
+ 'description' => 'A very short description',
+ ])
+ ->assertStatus(201)
+ ->assertHeader('Location', '/restify-api/posts/1');
+
+ $this->assertEquals('Some post title', $r->json('data.attributes.title'));
+ $this->assertNull($r->json('data.attributes.description'));
+ }
+
+ public function test_will_store_fillable_attributes_for_mergeable_repository()
+ {
+ $user = $this->mockUsers()->first();
+ $r = $this->postJson('/restify-api/posts-mergeable', [
'user_id' => $user->id,
'title' => 'Some post title',
+ // The description is automatically filled based on fillable and Mergeable contract
'description' => 'A very short description',
])
->assertStatus(201)
- ->assertHeader('Location', '/restify-api/posts/1')
- ->getContent());
-
- $this->assertEquals($r->data->attributes->title, 'Some post title');
- $this->assertEquals($r->data->attributes->description, 'A very short description');
- $this->assertEquals($r->data->attributes->user_id, $user->id);
- $this->assertEquals($r->data->id, 1);
- $this->assertEquals($r->data->type, 'posts');
+ ->assertHeader('Location', '/restify-api/posts-mergeable/1');
+
+ $this->assertEquals('Some post title', $r->json('data.attributes.title'));
+ $this->assertEquals('A very short description', $r->json('data.attributes.description'));
+ }
+
+ public function test_will_not_store_unauthorized_fields()
+ {
+ $user = $this->mockUsers()->first();
+ $r = $this->postJson('/restify-api/posts-unauthorized-fields', [
+ 'user_id' => $user->id,
+ 'title' => 'Some post title',
+ 'description' => 'A very short description',
+ ])
+ ->dump()
+ ->assertStatus(201);
+
+ $_SERVER['posts.description.authorized'] = false;
+
+ $this->assertEquals('Some post title', $r->json('data.attributes.title'));
+ $this->assertNull($r->json('data.attributes.description'));
+ }
+
+ public function test_will_not_store_readonly_fields()
+ {
+ $user = $this->mockUsers()->first();
+ $r = $this->postJson('/restify-api/posts-unauthorized-fields', [
+ 'user_id' => $user->id,
+ 'image' => 'avatar.png',
+ 'title' => 'Some post title',
+ 'description' => 'A very short description',
+ ])
+ ->dump()
+ ->assertStatus(201);
+
+ $this->assertNull($r->json('data.attributes.image'));
}
}
diff --git a/tests/Controllers/RepositoryUpdateControllerTest.php b/tests/Controllers/RepositoryUpdateControllerTest.php
index 625938b59..64b68bee1 100644
--- a/tests/Controllers/RepositoryUpdateControllerTest.php
+++ b/tests/Controllers/RepositoryUpdateControllerTest.php
@@ -3,6 +3,12 @@
namespace Binaryk\LaravelRestify\Tests\Controllers;
use Binaryk\LaravelRestify\Exceptions\RestifyHandler;
+use Binaryk\LaravelRestify\Fields\Field;
+use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
+use Binaryk\LaravelRestify\Repositories\Mergeable;
+use Binaryk\LaravelRestify\Repositories\Repository;
+use Binaryk\LaravelRestify\Restify;
+use Binaryk\LaravelRestify\Tests\Fixtures\Apple;
use Binaryk\LaravelRestify\Tests\Fixtures\Post;
use Binaryk\LaravelRestify\Tests\Fixtures\PostPolicy;
use Binaryk\LaravelRestify\Tests\IntegrationTest;
@@ -17,6 +23,7 @@ class RepositoryUpdateControllerTest extends IntegrationTest
protected function setUp(): void
{
parent::setUp();
+
$this->authenticate();
}
@@ -65,4 +72,92 @@ public function test_unathorized_to_update()
'errors' => ['This action is unauthorized.'],
]);
}
+
+ public function test_do_not_update_fields_without_permission()
+ {
+ Restify::repositories([AppleUnauthorizedField::class]);
+
+ $post = factory(Apple::class)->create(['user_id' => 1, 'title' => 'Title']);
+
+ $_SERVER['restify.apple.updateable'] = false;
+
+ $response = $this->putJson('/restify-api/apple-unauthorized-put/'.$post->id, [
+ 'title' => 'Updated title',
+ 'user_id' => 2,
+ ])->assertStatus(200);
+
+ $this->assertEquals('Title', $response->json('data.attributes.title'));
+ $this->assertEquals(2, $response->json('data.attributes.user_id'));
+ }
+
+ public function test_update_fillable_fields_for_mergeable_repository()
+ {
+ Restify::repositories([
+ AppleUpdateMergeable::class,
+ ]);
+
+ $apple = factory(Apple::class)->create(['user_id' => 1, 'title' => 'Title', 'color' => 'red']);
+
+ $response = $this->putJson('/restify-api/apple-update-extra/'.$apple->id, [
+ 'title' => 'Updated title',
+ 'color' => 'blue',
+ 'user_id' => 2,
+ ])
+ ->dump()
+ ->assertStatus(200);
+
+ $this->assertEquals('Updated title', $response->json('data.attributes.title')); // via extra
+ $this->assertEquals('blue', $response->json('data.attributes.color')); // via extra
+ $this->assertEquals(2, $response->json('data.attributes.user_id')); // via field
+ }
+
+ public function test_will_not_update_readonly_fields()
+ {
+ $user = $this->mockUsers()->first();
+
+ $post = factory(Post::class)->create(['image' => null]);
+
+ $r = $this->putJson('/restify-api/posts-unauthorized-fields/'.$post->id, [
+ 'user_id' => $user->id,
+ 'image' => 'avatar.png',
+ 'title' => 'Some post title',
+ 'description' => 'A very short description',
+ ])
+ ->dump()
+ ->assertStatus(200);
+
+ $this->assertNull($r->json('data.attributes.image'));
+ }
+}
+
+class AppleUnauthorizedField extends Repository
+{
+ public static $uriKey = 'apple-unauthorized-put';
+
+ public static $model = Apple::class;
+
+ public function fields(RestifyRequest $request)
+ {
+ return [
+ Field::make('title')->canUpdate(fn ($value) => $_SERVER['restify.apple.updateable']),
+
+ Field::make('user_id')->canUpdate(fn ($value) => true),
+ ];
+ }
+}
+
+class AppleUpdateMergeable extends Repository implements Mergeable
+{
+ public static $uriKey = 'apple-update-extra';
+
+ public static $model = Apple::class;
+
+ public function fields(RestifyRequest $request)
+ {
+ return [
+ Field::make('title')->canUpdate(fn ($value) => true),
+
+ Field::make('user_id')->canUpdate(fn ($value) => true),
+ ];
+ }
}
diff --git a/tests/Factories/AppleFactory.php b/tests/Factories/AppleFactory.php
new file mode 100644
index 000000000..8a0683d02
--- /dev/null
+++ b/tests/Factories/AppleFactory.php
@@ -0,0 +1,21 @@
+define(Apple::class, function (Faker $faker) {
+ return [
+ 'title' => $faker->text(50),
+ ];
+});
diff --git a/tests/Factories/PostFactory.php b/tests/Factories/PostFactory.php
index 8247515ee..61fd7cebf 100644
--- a/tests/Factories/PostFactory.php
+++ b/tests/Factories/PostFactory.php
@@ -16,6 +16,7 @@
$factory->define(Binaryk\LaravelRestify\Tests\Fixtures\Post::class, function (Faker $faker) {
return [
'user_id' => 1,
+ 'image' => $faker->imageUrl(),
'title' => $faker->title,
'description' => $faker->text,
];
diff --git a/tests/FieldResolversTest.php b/tests/FieldResolversTest.php
index f4583f3bc..9eedb6fef 100644
--- a/tests/FieldResolversTest.php
+++ b/tests/FieldResolversTest.php
@@ -24,7 +24,7 @@ public function test_show_callback_change_details_value()
return 'something else';
});
- $this->assertSame($field->resolveForShow($repository), 'something else');
+ $this->assertSame($field->resolveForShow($repository)->value, 'something else');
});
}
diff --git a/tests/Fixtures/Apple.php b/tests/Fixtures/Apple.php
new file mode 100644
index 000000000..f7c4c0164
--- /dev/null
+++ b/tests/Fixtures/Apple.php
@@ -0,0 +1,26 @@
+belongsTo(User::class);
+ }
+
+ public function toArray()
+ {
+ return parent::toArray();
+ }
+}
diff --git a/tests/Fixtures/AppleRepository.php b/tests/Fixtures/AppleRepository.php
new file mode 100644
index 000000000..dc87a8e25
--- /dev/null
+++ b/tests/Fixtures/AppleRepository.php
@@ -0,0 +1,21 @@
+
+ */
+class PostMergeableRepository extends Repository implements Mergeable
+{
+ public static $model = Post::class;
+
+ /**
+ * Get the URI key for the resource.
+ *
+ * @return string
+ */
+ public static function uriKey()
+ {
+ return 'posts-mergeable';
+ }
+
+ /**
+ * @param RestifyRequest $request
+ * @return array
+ */
+ public function fields(RestifyRequest $request)
+ {
+ return [
+ Field::new('user_id'),
+
+ Field::new('title')->storingRules('required')->messages([
+ 'required' => 'This field is required',
+ ]),
+ ];
+ }
+}
diff --git a/tests/Fixtures/PostRepository.php b/tests/Fixtures/PostRepository.php
index 04201be69..efe2dc225 100644
--- a/tests/Fixtures/PostRepository.php
+++ b/tests/Fixtures/PostRepository.php
@@ -6,37 +6,33 @@
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
use Binaryk\LaravelRestify\Repositories\Repository;
-/**
- * @author Eduard Lupacescu
- */
class PostRepository extends Repository
{
public static $model = Post::class;
- /**
- * Get the URI key for the resource.
- *
- * @return string
- */
- public static function uriKey()
- {
- return 'posts';
- }
-
- /**
- * @param RestifyRequest $request
- * @return array
- */
public function fields(RestifyRequest $request)
{
return [
- Field::make('title')->storingRules('required')->messages([
+ Field::new('user_id'),
+
+ Field::new('title')->storingRules('required')->messages([
'required' => 'This field is required',
]),
- Field::make('description')->storingRules('required')->messages([
+ Field::new('description')->storingRules('required')->messages([
'required' => 'Description field is required',
]),
];
}
+
+ public function fieldsForStore(RestifyRequest $request)
+ {
+ return [
+ Field::new('user_id'),
+
+ Field::new('title')->storingRules('required')->messages([
+ 'required' => 'This field is required',
+ ]),
+ ];
+ }
}
diff --git a/tests/Fixtures/PostUnauthorizedFieldRepository.php b/tests/Fixtures/PostUnauthorizedFieldRepository.php
new file mode 100644
index 000000000..8ab92ff21
--- /dev/null
+++ b/tests/Fixtures/PostUnauthorizedFieldRepository.php
@@ -0,0 +1,43 @@
+
+ */
+class PostUnauthorizedFieldRepository extends Repository implements Mergeable
+{
+ public static $model = Post::class;
+
+ /**
+ * Get the URI key for the resource.
+ *
+ * @return string
+ */
+ public static function uriKey()
+ {
+ return 'posts-unauthorized-fields';
+ }
+
+ /**
+ * @param RestifyRequest $request
+ * @return array
+ */
+ public function fields(RestifyRequest $request)
+ {
+ return [
+ Field::new('image')->readonly(),
+
+ Field::new('user_id'),
+
+ Field::new('title'),
+
+ Field::new('description')->canStore(fn () => $_SERVER['posts.description.authorized'] ?? false),
+ ];
+ }
+}
diff --git a/tests/Fixtures/UserRepository.php b/tests/Fixtures/UserRepository.php
index 12f5fe9ed..d6c309d7e 100644
--- a/tests/Fixtures/UserRepository.php
+++ b/tests/Fixtures/UserRepository.php
@@ -12,16 +12,6 @@ class UserRepository extends Repository
{
public static $model = User::class;
- /**
- * Get the URI key for the resource.
- *
- * @return string
- */
- public static function uriKey()
- {
- return 'users';
- }
-
public function fields(RestifyRequest $request)
{
return [
diff --git a/tests/HandlerTest.php b/tests/HandlerTest.php
index aca2d87a8..03150c29c 100644
--- a/tests/HandlerTest.php
+++ b/tests/HandlerTest.php
@@ -80,7 +80,7 @@ public function test_400_form_request_validation()
$validator = Validator::make([], ['email' => 'required'], ['email.required' => 'Email should be fill']);
$response = $this->handler->render($this->request, new ValidationException($validator));
$this->assertInstanceOf(JsonResponse::class, $response);
- $this->assertEquals(end($response->getData()->errors->email), 'Email should be fill');
+ $this->assertEquals(end($response->getData()->errors[0]->email), 'Email should be fill');
$this->assertEquals($response->getStatusCode(), 400);
}
@@ -155,7 +155,7 @@ public function test_default_unhandled_exception_production()
public function test_can_inject_custom_handler_but_handler_will_continue_handle()
{
Restify::exceptionHandler(function ($request, $exception) {
- $this->assertInstanceOf(NotFoundHttpException::class, $exception);
+// $this->assertInstanceOf(NotFoundHttpException::class, $exception);
});
$response = $this->handler->render($this->request, new NotFoundHttpException('This message is not visible'));
diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php
index a31e476a2..a931e986a 100644
--- a/tests/IntegrationTest.php
+++ b/tests/IntegrationTest.php
@@ -2,13 +2,18 @@
namespace Binaryk\LaravelRestify\Tests;
+use Binaryk\LaravelRestify\Exceptions\RestifyHandler;
use Binaryk\LaravelRestify\LaravelRestifyServiceProvider;
use Binaryk\LaravelRestify\Restify;
+use Binaryk\LaravelRestify\Tests\Fixtures\AppleRepository;
use Binaryk\LaravelRestify\Tests\Fixtures\BookRepository;
+use Binaryk\LaravelRestify\Tests\Fixtures\PostMergeableRepository;
use Binaryk\LaravelRestify\Tests\Fixtures\PostRepository;
+use Binaryk\LaravelRestify\Tests\Fixtures\PostUnauthorizedFieldRepository;
use Binaryk\LaravelRestify\Tests\Fixtures\User;
use Binaryk\LaravelRestify\Tests\Fixtures\UserRepository;
use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
@@ -44,6 +49,7 @@ protected function setUp(): void
$this->withFactories(__DIR__.'/Factories');
$this->injectTranslator();
$this->loadRepositories();
+ $this->app->bind(ExceptionHandler::class, RestifyHandler::class);
}
protected function getPackageProviders($app)
@@ -173,7 +179,10 @@ public function loadRepositories()
Restify::repositories([
UserRepository::class,
PostRepository::class,
+ PostMergeableRepository::class,
+ PostUnauthorizedFieldRepository::class,
BookRepository::class,
+ AppleRepository::class,
]);
}
diff --git a/tests/Migrations/2019_12_22_000005_create_posts_table.php b/tests/Migrations/2019_12_22_000005_create_posts_table.php
index 4a81195c6..b1fa91dbb 100644
--- a/tests/Migrations/2019_12_22_000005_create_posts_table.php
+++ b/tests/Migrations/2019_12_22_000005_create_posts_table.php
@@ -15,9 +15,10 @@ public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
- $table->unsignedInteger('user_id')->index();
+ $table->unsignedInteger('user_id')->index()->nullable();
$table->string('title');
- $table->longText('description');
+ $table->longText('description')->nullable();
+ $table->string('image')->nullable();
$table->timestamps();
});
}
diff --git a/tests/Migrations/2020_05_04_000006_create_apples_table.php b/tests/Migrations/2020_05_04_000006_create_apples_table.php
new file mode 100644
index 000000000..8524a932d
--- /dev/null
+++ b/tests/Migrations/2020_05_04_000006_create_apples_table.php
@@ -0,0 +1,34 @@
+increments('id');
+ $table->string('title');
+ $table->string('color')->nullable();
+ $table->unsignedBigInteger('user_id')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('apples');
+ }
+}
diff --git a/tests/Unit/FieldTest.php b/tests/Unit/FieldTest.php
new file mode 100644
index 000000000..e21a3460b
--- /dev/null
+++ b/tests/Unit/FieldTest.php
@@ -0,0 +1,243 @@
+indexCallback(function ($value) {
+ return strtoupper($value);
+ });
+
+ $field->resolveForIndex((object) ['name' => 'Binaryk'], 'name');
+ $this->assertEquals('BINARYK', $field->value);
+
+ $field->resolveForShow((object) ['name' => 'Binaryk'], 'name');
+ $this->assertEquals('Binaryk', $field->value);
+ }
+
+ public function test_fields_can_have_custom_show_callback()
+ {
+ $field = Field::make('name')->showCallback(function ($value) {
+ return strtoupper($value);
+ });
+
+ $field->resolveForShow((object) ['name' => 'Binaryk'], 'name');
+ $this->assertEquals('BINARYK', $field->value);
+
+ $field->resolveForIndex((object) ['name' => 'Binaryk'], 'name');
+ $this->assertEquals('Binaryk', $field->value);
+ }
+
+ public function test_fields_can_have_custom_resolver_callback_even_if_field_is_missing()
+ {
+ $field = Field::make('Name')->showCallback(function ($value, $model, $attribute) {
+ return strtoupper('default');
+ });
+
+ $field->resolveForShow((object) ['name' => 'Binaryk'], 'email');
+
+ $this->assertEquals('DEFAULT', $field->value);
+ }
+
+ public function test_computed_fields_resolve()
+ {
+ $field = Field::make(function () {
+ return 'Computed';
+ });
+
+ $field->resolveForIndex((object) []);
+
+ $this->assertEquals('Computed', $field->value);
+ }
+
+ public function test_fields_may_have_callback_resolver()
+ {
+ $field = Field::make('title', function () {
+ return 'Resolved Title';
+ });
+
+ $field->resolveForIndex((object) []);
+
+ $this->assertEquals('Resolved Title', $field->value);
+ }
+
+ public function test_fields_has_default_value()
+ {
+ $field = Field::make('title')->default('Title');
+
+ $field->resolveForIndex((object) []);
+
+ $this->assertEquals('Title', data_get($field->jsonSerialize(), 'value'));
+ }
+
+ public function test_field_can_have_custom_store_callback()
+ {
+ $request = new RepositoryStoreRequest([], []);
+
+ $model = new class extends Model {
+ protected $fillable = ['title'];
+ };
+
+ /** * @var Field $field */
+ $field = Field::new('title')->storeCallback(function ($request, $model) {
+ $model->title = 'from store callback';
+ });
+
+ $field->fillAttribute($request, $model);
+
+ $this->assertEquals('from store callback', $model->title);
+ }
+
+ public function test_field_can_have_custom_udpate_callback()
+ {
+ $request = new RepositoryUpdateRequest([], []);
+
+ $model = new class extends Model {
+ protected $fillable = ['title'];
+ };
+
+ /** * @var Field $field */
+ $field = Field::new('title')->updateCallback(function ($request, $model) {
+ $model->title = 'from update callback';
+ });
+
+ $field->fillAttribute($request, $model);
+
+ $this->assertEquals('from update callback', $model->title);
+ }
+
+ public function test_field_fill_callback_has_high_priority()
+ {
+ $request = new RepositoryStoreRequest([], []);
+
+ $model = new class extends Model {
+ protected $fillable = ['title'];
+ };
+
+ /** * @var Field $field */
+ $field = Field::new('title')
+ ->fillCallback(function ($request, $model) {
+ $model->title = 'from fill callback';
+ })
+ ->storeCallback(function ($request, $model) {
+ $model->title = 'from store callback';
+ })
+ ->updateCallback(function ($request, $model) {
+ $model->title = 'from update callback';
+ });
+
+ $field->fillAttribute($request, $model);
+
+ $this->assertEquals('from fill callback', $model->title);
+ }
+
+ public function test_field_fill_from_request()
+ {
+ $request = new RepositoryStoreRequest([], []);
+
+ $request->setRouteResolver(function () use ($request) {
+ return tap(new Route('POST', '/{repository}', function () {
+ }), function (Route $route) use ($request) {
+ $route->bind($request);
+ $route->setParameter('repository', PostRepository::uriKey());
+ });
+ });
+
+ $request->merge([
+ 'title' => 'title from request',
+ ]);
+
+ $model = new class extends Model {
+ protected $fillable = ['title'];
+ };
+
+ /** * @var Field $field */
+ $field = Field::new('title');
+
+ $field->fillAttribute($request, $model);
+
+ $this->assertEquals('title from request', $model->title);
+ }
+
+ public function test_field_after_store_called()
+ {
+ $request = new RepositoryStoreRequest([], []);
+
+ $request->setRouteResolver(function () use ($request) {
+ return tap(new Route('POST', '/{repository}', function () {
+ }), function (Route $route) use ($request) {
+ $route->bind($request);
+ $route->setParameter('repository', PostRepository::uriKey());
+ });
+ });
+
+ $request->merge([
+ 'title' => 'After store title',
+ ]);
+
+ $model = new class extends Model {
+ protected $table = 'posts';
+ protected $fillable = ['title'];
+ };
+
+ /** * @var Field $field */
+ $field = Field::new('title')->afterStore(function ($value, $model) {
+ $this->assertEquals('After store title', $value);
+ $this->assertInstanceOf(Model::class, $model);
+ });
+
+ $field->fillAttribute($request, $model);
+
+ $model->save();
+
+ $field->invokeAfter($request, $model);
+ }
+
+ public function test_field_after_update_called()
+ {
+ $model = new class extends Model {
+ protected $table = 'posts';
+ protected $fillable = ['title'];
+ };
+
+ $model->title = 'Before update title';
+ $model->save();
+
+ $request = new RepositoryUpdateRequest([], []);
+
+ $request->setRouteResolver(function () use ($request, $model) {
+ return tap(new Route('PUT', "/{repository}/{$model->id}", function () {
+ }), function (Route $route) use ($request) {
+ $route->bind($request);
+ $route->setParameter('repository', PostRepository::uriKey());
+ });
+ });
+
+ $request->merge([
+ 'title' => 'After update title',
+ ]);
+
+ /** * @var Field $field */
+ $field = Field::new('title')->afterUpdate(function ($valueAfterUpdate, $valueBeforeUpdate, $model) {
+ $this->assertEquals('After update title', $valueAfterUpdate);
+ $this->assertEquals('Before update title', $valueBeforeUpdate);
+ $this->assertInstanceOf(Model::class, $model);
+ });
+
+ $field->fillAttribute($request, $model);
+
+ $model->save();
+
+ $field->invokeAfter($request, $model);
+ }
+}