diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 24bb864..bbac2c4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,5 +5,5 @@ include: variables: TEST_PHP_8_3: "true" TEST_PHP_8_2: "true" - TEST_PHP_8_1: "true" + TEST_PHP_8_1: "false" TEST_PHP_8_0: "false" diff --git a/README.md b/README.md index c245104..aec000e 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ Collection of helpers for re-use accross a few of our projects - [Case insensitive statments](#case-insensitive-statments) - [Enforced Non Nullable Relations (orFail chain)](#enforced-non-nullable-relations-orfail-chain) - [DB Repositories](#db-repositories) - - [String Macros](#string-macros) - - [Observerable trait](#observerable-trait) + - [Observerable trait (Deprecated)](#observerable-trait-deprecated) - [Date Manipulation](#date-manipulation) - [Date(Carbon) Helpers attached to above:](#datecarbon-helpers-attached-to-above) - [Value Objects](#value-objects) @@ -31,6 +30,7 @@ Collection of helpers for re-use accross a few of our projects ## Installation + Install via composer ```bash @@ -123,17 +123,27 @@ use of repositories via extending the `CustomD\LaravelHelpers\Repository\BaseRep example in the [UserRepository.stub.php](https://git.customd.com/composer/Laravel-Helpers/-/blob/master/src/Repository/UserRepository.php.stub) file -## String Macros -`Str::reverse(string)` - to safely reverse a string that is multibyte safe. - -## Observerable trait +## Observerable trait (Deprecated) adding this trait to your models will automatically look for an observer in the app/Observers folder with the convension {model}Observer as the classname, you can additionally/optionally add ```php protected static $observers = [ ...arrayOfObservers] ``` -to add a additional ones if needed +to add a additional ones if + +Replace this with +``` +use Illuminate\Database\Eloquent\Attributes\ObservedBy; +use App\Observers\UserObserver; + +#[ObservedBy(UserObserver::class)] +#[ObservedBy(AnotherUserObserver::class)] +class User extends Model +{ + // +} +``` ## Date Manipulation @@ -179,8 +189,9 @@ methods available: * `usersStartOfYear(): Static` * `usersEndOfYear(): Static` * `parseWithTz(string $time): Static` - parses the time passed using the users timezone unless the timezone is in the timestamp +* `hasISOFormat(string $date): bool` - checks if the date is in iso format. -You can also use the CDCarbonDate to create a few differnt date objects. +You can also use the CDCarbonDate to create a few different date objects. ## Value Objects Example: @@ -192,11 +203,11 @@ namespace CustomD\LaravelHelpers\Tests\ValueObjects; use CustomD\LaravelHelpers\ValueObjects\ValueObject; -class SimpleValue extends ValueObject +final readonly class SimpleValue extends ValueObject { protected function __construct( - readonly public string $value, - readonly public int $count = 0 + public string $value, + public int $count = 0 ) { } @@ -226,7 +237,7 @@ use CustomD\LaravelHelpers\ValueObjects\Attributes\MakeableObject; use CustomD\LaravelHelpers\ValueObjects\Attributes\ChildValueObject; use CustomD\LaravelHelpers\ValueObjects\Attributes\CollectableValue; -class ComplexValue extends ValueObject +final readonly class ComplexValue extends ValueObject { public function __construct( #[ChildValueObject(StringValue::class)] @@ -246,6 +257,14 @@ class ComplexValue extends ValueObject Best practice is to use the make option, which will validate, if you use a public constructor it will not. +These should all be marked as READONLY and FINAL. + +The attributes available are: +* `ChildValueObject(valueobectclass)` - which will make a new valueObject +* `CollectableValue(valueobjectclass)` - which will convert an array to a coollection of the value objects +* `MakeableObject(class, [?$spread = false])` - will look for a make method or else construct if passed an non object - if spread is true will expand the array else will pass the array as a single argument + + ## Larastan Stubs **these are temporary only till implemented by larastan** diff --git a/composer.json b/composer.json index 5640f85..014aa08 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ } ], "require": { - "php": "^8.1", + "php": "^8.2", "illuminate/notifications": "^9.0|^10.0|^11.0", "illuminate/support": "^9.0|^10|^11.0" }, diff --git a/src/CdCarbonMixin.php b/src/CdCarbonMixin.php index 699a1d7..9ba9ddb 100644 --- a/src/CdCarbonMixin.php +++ b/src/CdCarbonMixin.php @@ -194,4 +194,9 @@ public function usersFormat(): Closure return $date->setTimezone($date->getUserTimezone())->format($format); }; } + + public function hasISOFormat(): callable + { + return static fn ($date): bool => Carbon::hasFormat($date, 'Y-m-d\TH:i:s.u\Z'); + } } diff --git a/src/Database/Query/Mixins/NullOrEmptyMixin.php b/src/Database/Query/Mixins/NullOrEmptyMixin.php index 5e0b7c6..8fa85df 100644 --- a/src/Database/Query/Mixins/NullOrEmptyMixin.php +++ b/src/Database/Query/Mixins/NullOrEmptyMixin.php @@ -12,7 +12,7 @@ class NullOrEmptyMixin public function whereNullOrEmpty(): Closure { - return function (string $column) { + return function (string $column): Builder { /** @var \Illuminate\Database\Query\Builder $this */ return $this->where(fn (Builder $builder) => $builder->where($column, '=', '')->orWhereNull($column)); }; @@ -20,7 +20,7 @@ public function whereNullOrEmpty(): Closure public function orWhereNullOrEmpty(): Closure { - return function (string $column) { + return function (string $column): Builder { /** @var \Illuminate\Database\Query\Builder $this */ return $this->orWhere(fn (Builder $builder) => $builder->where($column, '=', '')->orWhereNull($column)); }; @@ -29,7 +29,7 @@ public function orWhereNullOrEmpty(): Closure public function whereNotNullOrEmpty(): Closure { - return function (string $column) { + return function (string $column): Builder { /** @var \Illuminate\Database\Query\Builder $this */ return $this->where(fn(Builder $builder) => $builder->where($column, '!=', '')->whereNotNull($column)); }; @@ -37,7 +37,7 @@ public function whereNotNullOrEmpty(): Closure public function orWhereNotNullOrEmpty(): Closure { - return function (string $column) { + return function (string $column): Builder { /** @var \Illuminate\Database\Query\Builder $this */ return $this->orWhere(fn(Builder $builder) => $builder->where($column, '!=', '')->whereNotNull($column)); }; @@ -46,13 +46,41 @@ public function orWhereNotNullOrEmpty(): Closure public function whereNullOrValue(): Closure { /** @param $value mixed **/ - return function (string $column, $operator = null, $value = null, $boolean = 'and') { + return function (string $column, $operator = null, $value = null, $boolean = 'and'): Builder { /** @var \Illuminate\Database\Query\Builder $this */ [$value, $operator] = $this->prepareValueAndOperator( - $value, $operator, func_num_args() === 2 + $value, + $operator, + func_num_args() === 2 ); return $this->where(fn (Builder $builder) => $builder->whereNull($column)->when($value, fn($sbuilder) => $sbuilder->orWhere($column, $operator, $value, $boolean))); }; } + + public function iWhere(): Closure + { + return function (string|array $column, $operator = null, $value = null, $boolean = 'and'): Builder { + /** @var \Illuminate\Database\Query\Builder $this */ + if (is_array($column)) { + return $this->addArrayOfWheres($column, $boolean, 'iWhere'); //@phpstan-ignore-line + } + + [$value, $operator] = $this->prepareValueAndOperator( + $value, + $operator, + func_num_args() === 2 + ); + + return $this->whereRaw("LOWER({$column}) {$operator} ?", [strtolower($value)], $boolean); + }; + } + + public function orIWhere(): Closure + { + return function (string|array $column, $operator = null, $value = null): Builder { + /** @var \Illuminate\Database\Query\Builder $this */ + return $this->iWhere($column, $operator, $value, 'or'); + }; + } } diff --git a/src/Http/Middleware/UserTimeZone.php b/src/Http/Middleware/UserTimeZone.php index 6284f08..e036a3b 100644 --- a/src/Http/Middleware/UserTimeZone.php +++ b/src/Http/Middleware/UserTimeZone.php @@ -1,6 +1,7 @@ permission_name ?? self::parsePermissionNameFromPolicy(), $action ])->filter()->implode("."); - if ($model && method_exists($model, 'userHasPermission')) - { - info('Model::userHasPermission calls have been deprecated - and will be removed in the next version'); - if(! $model->userHasPermission($user)) { - return false; + if ($model) { + $can = $this->canOnModel($user, $permission, $model); + if ($can === true) { + return $can; + } + $can = $this->canOnModelField($user, $permission, $model); + if ($can === true) { + return $can; } } - return $user->can($permission); - } - public static function parsePermissionNameFromPolicy(): string - { - return Str::of(class_basename(get_called_class())) - ->replaceLast('Policy', '') - ->snake() - ->plural() - ->value; + return $user->can($permission); } /** - * Determine whether the user can view any models. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @return mixed + * @still in development */ - public function viewAny(Authenticatable $user) + protected function canOnModel(Authenticatable&Authorizable $user, string $permission, Model $model): ?true { - return $this->can($user, 'viewAny'); - } - /** - * Determine whether the user can view the model. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed - */ - public function view(Authenticatable $user, Model $model) - { - return $this->can($user, 'view'); - } + if (property_exists($this, 'ownerIdColumn') === false) { + return null; + } - /** - * Determine whether the user can create models. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @return mixed - */ - public function create(Authenticatable $user) - { - return $this->can($user, 'create'); + $ownerId = $model->getAttribute($this->ownerIdColumn); + if ($ownerId === $user->getAuthIdentifier()) { + return true; + } + + return null; } /** - * Determine whether the user can update the model. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed + * @still in development */ - public function update(Authenticatable $user, Model $model) + protected function canOnModelField(Authenticatable&Authorizable $user, string $permission, Model $model): ?bool { - return $this->can($user, 'update'); + if (property_exists($this, 'modelField') === false || $this->modelField === false) { + return null; + } + + if ($this->modelField === true || $this->modelField === '*') { + $permission .= ".*"; + } else { + $permission .= "." . $model->getAttribute($this->modelField); + } + + return $user->can($permission); } - /** - * Determine whether the user can delete the model. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed - */ - public function delete(Authenticatable $user, Model $model) + public static function parsePermissionNameFromPolicy(): string { - return $this->can($user, 'delete'); + return Str::of(class_basename(get_called_class())) + ->replaceLast('Policy', '') + ->snake() + ->plural() + ->value; } - /** - * Determine whether the user can restore the model. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed - */ - public function restore(Authenticatable $user, Model $model) + public function __call($method, $parameters) { - return $this->can($user, 'restore'); + return $this->can($parameters[0], $method, $parameters[1] ?? null); } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 9989869..7031c22 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -2,25 +2,20 @@ namespace CustomD\LaravelHelpers; -use Closure; use Carbon\Carbon; use Carbon\CarbonImmutable; -use Illuminate\Support\Str; use Illuminate\Database\Query\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Http; -use CustomD\LaravelHelpers\CdCarbonDate; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\ModelNotFoundException; use CustomD\LaravelHelpers\Database\Query\Mixins\NullOrEmptyMixin; -use CustomD\LaravelHelpers\Facades\CdCarbonDate as FacadesCdCarbonDate; class ServiceProvider extends \Illuminate\Support\ServiceProvider { public function register() { - Carbon::mixin(new CdCarbonMixin()); CarbonImmutable::mixin(new CdCarbonMixin()); } @@ -34,14 +29,17 @@ public function register() public function boot() { $this->registerDbMacros(); - $this->registerStringMacros(); \Illuminate\Database\Eloquent\Factories\Factory::macro('randomTestingId', function ($min = 1000, $max = 1000000) { /** @var \Illuminate\Database\Eloquent\Factories\Factory $this*/ return \Illuminate\Support\Facades\App::runningUnitTests() ? $this->faker->unique()->numberBetween($min, $max) : null; //@phpstan-ignore-line }); - Http::macro('enableRecording', fn() => $this->record()); //@phpstan-ignore-line + /** @macro Http */ + Http::macro('enableRecording', function () { + /** @var \Illuminate\Http\Client\Factory $this*/ + return $this->record(); //@phpstan-ignore-line + }); } @@ -64,42 +62,5 @@ function (?string $error = null) { ); } ); - - /** @macro \Illuminate\Database\Query\Builder */ - Builder::macro('iWhere', function ($column, $operator = null, $value = null, $boolean = 'and') { - /** @var \Illuminate\Database\Query\Builder $this */ - if (is_array($column)) { - return $this->addArrayOfWheres($column, $boolean, 'iWhere'); //@phpstan-ignore-line - } - - [$value, $operator] = $this->prepareValueAndOperator( - $value, - $operator, - func_num_args() === 2 - ); - - return $this->whereRaw("LOWER({$column}) {$operator} ?", [strtolower($value)], $boolean); - }); - - /** @macro \Illuminate\Database\Query\Builder */ - Builder::macro('orIWhere', function ($column, $operator = null, $value = null) { - /** @var \Illuminate\Database\Query\Builder $this */ - [$value, $operator] = $this->prepareValueAndOperator( - $value, - $operator, - func_num_args() === 2 - ); - - return $this->iWhere($column, $operator, $value, 'or'); - }); - } - - protected function registerStringMacros(): void - { - /** @macro \Illuminate\Support\Str */ - Str::macro('reverse', function ($string, $encoding = null) { - $chars = mb_str_split($string, 1, $encoding ?? mb_internal_encoding()); - return implode('', array_reverse($chars)); - }); } } diff --git a/src/Traits/Observerable.php b/src/Traits/Observerable.php index 735868f..beec2e0 100644 --- a/src/Traits/Observerable.php +++ b/src/Traits/Observerable.php @@ -2,6 +2,9 @@ namespace CustomD\LaravelHelpers\Traits; +/** + * @deprecated -- start using the attribute key + */ trait Observerable { diff --git a/src/ValueObjects/ValueObject.php b/src/ValueObjects/ValueObject.php index 20ac67c..a9a59a2 100644 --- a/src/ValueObjects/ValueObject.php +++ b/src/ValueObjects/ValueObject.php @@ -15,7 +15,7 @@ /** * @implements Arrayable */ -abstract class ValueObject implements Arrayable +abstract readonly class ValueObject implements Arrayable { /** @@ -99,7 +99,7 @@ public function toArray(): array ->toArray(); } - public function __toString() + public function toJsonString(): string { return json_encode($this->toArray(), JSON_THROW_ON_ERROR); } diff --git a/src/helpers.php b/src/helpers.php index 2034ebd..7e98ed3 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -50,20 +50,19 @@ function dumph(...$vars) } } -if(!function_exists('from_key_or_model')) -{ +if (! function_exists('from_key_or_model')) { /** * @template TType * @param string|int|\Illuminate\Database\Eloquent\Model $value * @param class-string $type - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException * @return \Illuminate\Database\Eloquent\Model|TType */ - function from_key_or_model(string|int|\Illuminate\Database\Eloquent\Model $value, string $type) - { - if(is_object($value) && is_a($value, $type)){ - return $value; - } - return $type::whereKey($value)->firstOrFail(); - } + function from_key_or_model(string|int|\Illuminate\Database\Eloquent\Model $value, string $type) + { + if (is_object($value) && is_a($value, $type)) { + return $value; + } + return $type::whereKey($value)->firstOrFail(); + } } diff --git a/tests/CrudPermissionTest.php b/tests/CrudPermissionTest.php new file mode 100644 index 0000000..4d0fec2 --- /dev/null +++ b/tests/CrudPermissionTest.php @@ -0,0 +1,31 @@ + true); + Gate::define('model_ones.view', fn() => true); + Gate::define('model_ones.fetch', fn() => false); + + $this->assertTrue($policy->viewAny($user)); + $this->assertFalse($policy->fetch($user, $model)); + $this->assertTrue($policy->view($user, $model)); + $this->assertFalse($policy->somemethod($user, $model)); + } +} diff --git a/tests/DatabaseMacrosTest.php b/tests/DatabaseMacrosTest.php index 206b37b..a93e49f 100644 --- a/tests/DatabaseMacrosTest.php +++ b/tests/DatabaseMacrosTest.php @@ -65,12 +65,19 @@ public function test_or_where_not_null_or_empty() public function test_case_insensitive_iwhere() { - $query = ModelOne::iWhere('name', 'TestCom')->iWhere(['company' => 'my-comPanbyName'])->orIWhere('country', '!=', 'Nz'); + $query = ModelOne::iWhere('name', 'TestCom') + ->iWhere(['company' => 'my-comPanbyName']) + ->orIWhere('country', '!=', 'Nz') + ->orIWhere([ + 'col' => 'umn', + 'mnot' => 'tonm', + ['asdf' ,'!=', 'asdf', 'or'], + ]); $this->assertStringContainsString('LOWER(name) = ?', $query->toSql()); $this->assertStringContainsString('or LOWER(country) != ?', $query->toSql()); $this->assertStringContainsString('LOWER(company) = ?', $query->toSql()); - $this->assertEquals(['testcom','my-companbyname','nz'], $query->getBindings()); + $this->assertTrue(str($query->toRawSql())->contains("where LOWER(name) = 'testcom' and (LOWER(company) = 'my-companbyname') or LOWER(country) != 'nz' or (LOWER(col) = 'umn' or LOWER(mnot) = 'tonm' or LOWER(asdf) != 'asdf')")); } public function test_has_one_nullable() diff --git a/tests/ModelOnePolicy.php b/tests/ModelOnePolicy.php new file mode 100644 index 0000000..fd8e000 --- /dev/null +++ b/tests/ModelOnePolicy.php @@ -0,0 +1,9 @@ +