From fbe768dc17e775eb14ba748708288c41992a7a30 Mon Sep 17 00:00:00 2001 From: DarkGhostHunter Date: Sun, 24 May 2020 14:30:08 -0400 Subject: [PATCH 1/6] Reworked whole package for Laravel 7. Tests pending. --- .github/workflows/php.yml | 4 +- README.md | 104 ++++---- composer.json | 7 +- src/ClassDiscoverer.php | 232 ------------------ src/Controllers/ThrottlesRequests.php | 168 ------------- src/{Models => Eloquent}/AutoFill.php | 29 ++- src/Eloquent/Casts/CastEnumerable.php | 74 ++++++ .../Casts/CastsRepository.php} | 52 ++-- src/{Models => Eloquent}/DefaultColumns.php | 29 ++- src/{Models => Eloquent}/HasSlug.php | 42 ++-- src/{Models => Eloquent}/ModelType.php | 47 +++- src/{Models => Eloquent}/NeighbourRecords.php | 101 ++++++-- .../SoftCachesAccessors.php | 36 ++- src/{Models => Eloquent}/UsesUuid.php | 18 +- src/Enumerable.php | 59 ++--- src/EnumerableStates.php | 75 ++++-- src/{DiscoverClasses.php => FiresItself.php} | 45 ++-- src/InfiniteHigherOrderTapProxy.php | 2 +- src/Jobs/DispatchablePipeline.php | 1 + src/Middleware/CacheStaticResponse.php | 10 + src/Middleware/ShareAuthenticatedUser.php | 21 +- .../ValidateConsumableSignature.php | 28 +-- src/Models/HasFile.php | 220 ----------------- src/Multitaps.php | 2 +- src/PipesThrough.php | 21 +- src/RendersFromMarkdown.php | 9 +- src/SavesToCache.php | 30 +-- src/SavesToSession.php | 29 +-- src/SavesToStorage.php | 24 +- src/Scopes/DefaultColumns.php | 8 +- src/Scopes/MacrosEloquent.php | 20 +- src/Scopes/UuidScope.php | 131 +++++----- src/SendsToHttp.php | 101 ++++++++ .../RegisterBladeExtensions.php | 87 +++++++ .../RegisterGates.php} | 53 ++-- src/ServiceProviders/RegistersObservers.php | 65 +++++ src/ValidatesItself.php | 7 +- tests/DiscoverClassesTest.php | 70 ------ tests/DispatchesItselfTest.php | 45 ---- tests/EnumerableStatesTest.php | 8 +- tests/EnumerableTest.php | 6 +- tests/Models/AutoFillTest.php | 2 +- tests/Models/DefaultColumnsTest.php | 2 +- tests/Models/DynamicallyMutatesTest.php | 2 +- tests/Models/HasFileTest.php | 2 +- tests/Models/HasSlugTest.php | 4 +- tests/Models/ModelTypeTest.php | 6 +- tests/Models/NeighbourRecordsTest.php | 2 +- tests/Models/SoftCachesAccessorsTest.php | 4 +- tests/Models/UsesUuidTest.php | 2 +- 50 files changed, 931 insertions(+), 1215 deletions(-) delete mode 100644 src/ClassDiscoverer.php delete mode 100644 src/Controllers/ThrottlesRequests.php rename src/{Models => Eloquent}/AutoFill.php (76%) create mode 100644 src/Eloquent/Casts/CastEnumerable.php rename src/{DispatchesItself.php => Eloquent/Casts/CastsRepository.php} (51%) rename src/{Models => Eloquent}/DefaultColumns.php (72%) rename src/{Models => Eloquent}/HasSlug.php (74%) rename src/{Models => Eloquent}/ModelType.php (65%) rename src/{Models => Eloquent}/NeighbourRecords.php (50%) rename src/{Models => Eloquent}/SoftCachesAccessors.php (72%) rename src/{Models => Eloquent}/UsesUuid.php (84%) rename src/{DiscoverClasses.php => FiresItself.php} (55%) delete mode 100644 src/Models/HasFile.php create mode 100644 src/SendsToHttp.php create mode 100644 src/ServiceProviders/RegisterBladeExtensions.php rename src/{Models/DynamicallyMutates.php => ServiceProviders/RegisterGates.php} (54%) create mode 100644 src/ServiceProviders/RegistersObservers.php delete mode 100644 tests/DiscoverClassesTest.php delete mode 100644 tests/DispatchesItselfTest.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 56296e7..a6da838 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -12,13 +12,11 @@ jobs: fail-fast: true matrix: php: [7.4, 7.3, 7.2.15] - laravel: [7.*, 6.*] + laravel: [7.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 7.* testbench: 5.* - - laravel: 6.* - testbench: 4.* name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} diff --git a/README.md b/README.md index 33bc266..034f84e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Laratraits is a Laravel package containing useful traits and some classes to use ## Requirements -* Laravel 6, or Laravel 7. +* Laravel 7. * PHP 7.2.15 or later. ## Installation @@ -20,57 +20,7 @@ Fire up Composer and that's it. composer require darkghosthunter/laratraits -## What it includes - -Before installing, take a look into the list. If you're only using one, just copy and paste it in your project, no problem, as each trait and file includes a copy of the MIT License. Just remember to change the namespace. - -### General - -* [`DiscoverClasses`](src/DiscoverClasses.php): Discovers classes inside a directory, optionally filtered by a method name or interface. -* [`DispatchesItself`](src/DispatchesItself.php): Allows to dispatch the object instance to one of many Jobs previously set. -* [`Multitaps`](src/Multitaps.php): Makes a class infinitely tap-able. You can exit the tap using `->target` or a method name appended with `AndUntap`. -* [`RendersFromMarkdown`](src/RendersFromMarkdown.php): Takes a given class property to parse Markdown text and return HTML. Compatible with `Htmlable` interface. -* [`ValidatesItself`](src/ValidatesItself.php): Validates an incoming data using self-contained rules. -* [`SavesToSession`](src/SavesToSession.php): Saves the object (or part of it) to the session. -* [`SavesToCache`](src/SavesToCache.php): Saves the object (or part of it) to the cache..php -* [`SavesToStorage`](src/SavesToStorage.php): Saves the object (or part of it) to the storage. -* [`EnumerableStates`](src/EnumerableStates.php): Allows a class to have one of an strict list of possible states. You can also use the [`Enumerable`](src/Enumerable.php) class separately. Useful for [casting enums](https://laravel.com/docs/eloquent-mutators#custom-casts). - -### Models - -* [`AutoFill`](src/Models/AutoFill.php): Automatically fills the Model with values by each method name, like `fillFooAttribute()`. -* [`UsesUuid`](src/Models/UsesUuid.php): Automatically fills the UUID on the Model. Comes with an optional Eloquent Query Builder local scopes. You can override the UUID generation. -* [`DefaultColumns`](src/Models/DefaultColumns.php): Adds a `DefaultColumns` Global Scope to the Model that selects only given default columns, unless overrun manually in the query. -* [`SoftCachesAccessors`](src/Models/SoftCachesAccessors.php): Saves the result of a accessor to avoid running the accessor logic again. Overrides the `mutateAttribute()` method. -* [`DynamicallyMutates`](src/Models/DynamicallyMutates.php): Cast an attribute based on what other attribute type says. Useful for columns that hols the data type, and other the raw data value. -* [`NeighbourRecords`](src/Models/NeighbourRecords.php): Allows to easily get the "next" and "previous" record from a given model. -* [`HasSlug`](src/Models/HasSlug.php): Allows a Model to be bound to routes using the slug like `this-is-the-model`. Requires a new column in the table. -* [`ModelType`](src/Models/ModelType.php): Useful for Models that share a single table but a different "column type". -* [`HasFile`](src/Models/HasFile.php): Associates a single file to the Model. The File contents is automatically saved when model is persisted/updated. Hash checking is always done. - -### Pipelines - -* [`PipesThrough`](src/PipesThrough.php): Allows any class to be sent itself through a pipeline or, alternatively, queue a Job to process the pipeline later. - -### Global Scopes - -* [`MacrosEloquent`](src/Scopes/MacrosEloquent.php): Automatically adds selective Macros to the Eloquent Builder instance itself, instead of globally, when using a Global Scope. Append a method with "macro" and return a Closure to use as macro. - -### Controllers - -* [`ThrottlesRequests`](src/Controllers/ThrottlesRequests.php): An automatic and customizable request throttler, much like the default `ThrottlesLogins` trait. - -### Middleware - -* [`CacheStaticResponse`](src/Middleware/CacheStaticResponse.php): Caches (hopefully) static responses, avoiding running the controller logic, for a given time. -* [`ShareAuthenticatedUser`](src/Middleware/ShareAuthenticatedUser.php): Shares the authenticated user across all views. -* [`ValidateConsumableSignature`](src/Middleware/ValidateConsumableSignature.php): Makes signed routes work only one time except on client or server errors. - -## Installing - -Just fire up composer and that's it. - - composer require darkghosthunter/laratraits +This package doesn't uses a Service Provider. ## Usage @@ -85,7 +35,7 @@ namespace App; use Illuminate\Database\Eloquent\Model; use DarkGhostHunter\Laratraits\SavesToCache; -use DarkGhostHunter\Laratraits\Models\UsesUuid; +use DarkGhostHunter\Laratraits\Eloquent\UsesUuid; class Post extends Model { @@ -96,9 +46,53 @@ class Post extends Model } ``` -Some traits may instance other classes for advanced logic if necessary, like the `DiscoverClasses` trait. -> There is no application overhead since there is no Service Provider registered. +## What it includes + +Before installing, take a look into the list. If you're only using one, just copy and paste it in your project, no problem, as each trait and file includes a copy of the MIT License. . + +Just remember to **change the namespace** if you're copy-pasting them! + +### Traits for everyone + +* [`EnumerableStates`](src/EnumerableStates.php): Allows a class instance to have a single allowed state. +* [`Multitaps`](src/Multitaps.php): Makes all class methods _chainable_, like using `tap()` but forever. You can exit the tap using `->target` or a method name appended with `AndUntap`. +* [`PipesThrough`](src/PipesThrough.php): Allows a class to be piped through a pipeline immediately or to a queue. +* [`RendersFromMarkdown`](src/RendersFromMarkdown.php): Takes a given class property to parse Markdown text and return HTML. Compatible with `Htmlable` interface. +* [`SavesToCache`](src/SavesToCache.php): Saves the object (or part of it) to the cache. +* [`SavesToSession`](src/SavesToSession.php): Saves the object (or part of it) to the session. +* [`SavesToStorage`](src/SavesToStorage.php): Saves the object (or part of it) to the storage. +* [`SendsToHttp`](src/SendsToHttp.php): Sends the object (or part of it) through an HTTP Request. +* [`ValidatesItself`](src/ValidatesItself.php): Validates an incoming data using self-contained rules. + +### Useful classes + +* [`Enumerable`](src/Enumerable.php): Lists and controls a state from a list. Useful for [casting enums](https://laravel.com/docs/eloquent-mutators#custom-casts). + +### Models + +* [`AutoFill`](src/Eloquent/AutoFill.php): Automatically fills the Model with values by each method name, like `fillFooAttribute()`. +* [`DefaultColumns`](src/Eloquent/DefaultColumns.php): Adds a `DefaultColumns` Global Scope to the Model that selects only given default columns, unless overrun manually in the query. +* [`HasSlug`](src/Eloquent/HasSlug.php): Allows a Model to be bound to routes using the slug like `this-is-the-model`. Requires a new column in the table. +* [`ModelType`](src/Eloquent/ModelType.php): Useful for Models that share a single table but have different "types". +* [`NeighbourRecords`](src/Eloquent/NeighbourRecords.php): Allows to easily get the "next" and "previous" record from a given model. +* [`SoftCachesAccessors`](src/Eloquent/SoftCachesAccessors.php): Saves the result of a accessor to avoid running the accessor logic again. Overrides the `mutateAttribute()` method. +* [`UsesUuid`](src/Eloquent/UsesUuid.php): Automatically fills the UUID on the Model. Comes with an optional Eloquent Query Builder local scopes. You can override the UUID generation. + +#### Casts + +* [`CastEnumerable`](src/Eloquent/Casts/CastEnumerable.php): Allows a custom Enumerable class to be [_castable_](https://laravel.com/docs/eloquent-mutators#custom-casts) in a model. +* [`CastsRepository`](src/Eloquent/Casts/CastsRepository.php): Allows an array property to be cased as a Repository. + +### Global Scopes + +* [`MacrosEloquent`](src/Scopes/MacrosEloquent.php): Automatically adds selective Macros to the Eloquent Builder instance itself, instead of globally, when using a Global Scope. Append `macro` to a public static method and that's it. + +### Middleware + +* [`CacheStaticResponse`](src/Middleware/CacheStaticResponse.php): Caches static responses, avoiding running the controller logic, for a given time. +* [`ShareAuthenticatedUser`](src/Middleware/ShareAuthenticatedUser.php): Shares the authenticated user across all views. +* [`ValidateConsumableSignature`](src/Middleware/ValidateConsumableSignature.php): Makes [signed routes](https://laravel.com/docs/urls#signed-urls) work only one time except on client or server errors. ## Missing a trait? diff --git a/composer.json b/composer.json index d88aa30..9d2f725 100644 --- a/composer.json +++ b/composer.json @@ -22,11 +22,11 @@ "require": { "php": "^7.2.15", "ext-json": "*", - "illuminate/support": "6.*|7.*", - "symfony/finder": "4.*|5.*" + "illuminate/support": "7.*", + "symfony/finder": "5.*" }, "require-dev": { - "orchestra/testbench": "~4.1|5.*" + "orchestra/testbench": "5.*" }, "autoload": { "psr-4": { @@ -41,7 +41,6 @@ "scripts": { "test": "vendor/bin/phpunit", "test-coverage": "vendor/bin/phpunit --coverage-html coverage" - }, "config": { "sort-packages": true diff --git a/src/ClassDiscoverer.php b/src/ClassDiscoverer.php deleted file mode 100644 index 80ba911..0000000 --- a/src/ClassDiscoverer.php +++ /dev/null @@ -1,232 +0,0 @@ -path = $app->path(); - $this->basePath = trim($app->basePath(), DS); - } - - /** - * Path to look for inside a directory, relative to the base path. - * - * @param string $path - * @return \DarkGhostHunter\Laratraits\ClassDiscoverer - */ - public function path(string $path) - { - $this->path = $this->basePath . DS . trim(str_replace(['\\', '/'], DS, $path), DS); - - // Fix the path root if its linux and the initial slash is not present. - if (ctype_lower($this->path[0])) { - $this->path = Str::start($this->path, DS); - } - - return $this; - } - - /** - * Filters the class list by only those containing a public method. - * - * @param string $method - * @return \DarkGhostHunter\Laratraits\ClassDiscoverer - */ - public function filterByMethod(string $method) - { - $this->filter = [ - 'type' => 'filterMethod', - 'value' => $method, - ]; - - return $this; - } - - /** - * Filter the class list by only those implementing a given interface. - * - * @param string $interface - * @return $this - */ - public function filterByInterface(string $interface) - { - if (! interface_exists($interface)) { - throw new InvalidArgumentException("The interface [$interface] has not been declared."); - } - - $this->filter = [ - 'type' => 'filterInterface', - 'value' => $interface, - ]; - - return $this; - } - - /** - * Returns the Class names. - * - * @return \Illuminate\Support\Collection - */ - public function discover() - { - $classes = collect($this->getFiles()) - ->map([$this, 'filterClasses']) - ->filter(); - - if ($this->filter) { - $classes = $classes->filter([$this, $this->filter['type']]); - } - - return $classes->map->name; - } - - /** - * Get the files for the given path - * - * @return \Symfony\Component\Finder\Finder - */ - protected function getFiles() - { - return (new Finder)->files()->in($this->path); - } - - /** - * Returns a Reflection Class from the SplFileInfo File - * - * @param \SplFileInfo $file - * @return null|\ReflectionClass - */ - public function filterClasses(SplFileInfo $file) - { - try { - $class = new ReflectionClass($this->classFromFile($file)); - } - catch (ReflectionException $e) { - return null; - } - - if (! $class->isInstantiable()) { - return null; - } - - return $class; - } - - /** - * Extract the class name from the given file path. - * - * @param \SplFileInfo $file - * @return string - */ - protected function classFromFile(SplFileInfo $file) - { - $class = trim(Str::replaceFirst($this->basePath, '', $file->getRealPath()), DS); - - return ucfirst(Str::replaceLast('.php', '', $class)); - } - - /** - * Filter each class by a method name. - * - * @param \ReflectionClass $class - * @return bool - */ - public function filterMethod(ReflectionClass $class) - { - foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { - if (strpos($method->name, $this->filter['value']) === 0) { - return true; - } - } - - return false; - } - - /** - * Filter each class by a given implementation. - * - * @param \ReflectionClass $class - * @return bool - */ - public function filterInterface(ReflectionClass $class) - { - if (in_array($this->filter['value'], $class->getInterfaceNames(), true)) { - return true; - } - - return false; - } -} diff --git a/src/Controllers/ThrottlesRequests.php b/src/Controllers/ThrottlesRequests.php deleted file mode 100644 index 8cd8053..0000000 --- a/src/Controllers/ThrottlesRequests.php +++ /dev/null @@ -1,168 +0,0 @@ -hasTooManyAttempts($request, $attempts)) { - if (method_exists($this, 'fireThrottledEvent')) { - $this->fireThrottledEvent($request); - } - - $this->sendThrottledResponse($request); - } - } - - /** - * Determine if the user has attempted too many times. - * - * @param \Illuminate\Http\Request $request - * @param int $attempts - * @return bool - */ - protected function hasTooManyAttempts(Request $request, int $attempts = null) - { - return $this->limiter()->tooManyAttempts( - $this->throttleKey($request), $attempts ?? $this->maxAttempts() - ); - } - - /** - * Increment the attempts for the user. - * - * @param \Illuminate\Http\Request $request - * @param int|null $decay - * @return void - */ - protected function incrementAttempts(Request $request, int $decay = null) - { - $this->limiter()->hit( - $this->throttleKey($request), ($decay ?? $this->decayMinutes()) * 60 - ); - } - - /** - * Redirect the user after determining he is throttled. - * - * @param \Illuminate\Http\Request $request - * @return \Symfony\Component\HttpFoundation\Response|void - * - * @throws \Illuminate\Validation\ValidationException - */ - protected function sendThrottledResponse(Request $request) - { - throw ValidationException::withMessages([ - Lang::get('auth.throttle', ['seconds' => $this->availableIn($request)]) - ])->status(Response::HTTP_TOO_MANY_REQUESTS); - } - - /** - * Returns how many seconds until the request can be retried again - * - * @param \Illuminate\Http\Request $request - * @return integer - */ - protected function availableIn(Request $request) - { - return $this->limiter()->availableIn($this->throttleKey($request)); - } - - /** - * Get the throttle key for the given request. - * - * @param \Illuminate\Http\Request $request - * @return string - */ - protected function throttleKey(Request $request) - { - return 'request|throttle|' . $request->fingerprint(); - } - - /** - * Get the rate limiter instance. - * - * @return \Illuminate\Cache\RateLimiter - */ - protected function limiter() - { - return $this->limiter = $this->limiter ?? app(RateLimiter::class); - } - - /** - * Get the maximum number of attempts to allow. - * - * @return int - */ - protected function maxAttempts() - { - return $this->maxAttempts ?? 5; - } - - /** - * Get the number of minutes to throttle for. - * - * @return int - */ - public function decayMinutes() - { - return $this->decayMinutes ?? 1; - } -} diff --git a/src/Models/AutoFill.php b/src/Eloquent/AutoFill.php similarity index 76% rename from src/Models/AutoFill.php rename to src/Eloquent/AutoFill.php index 8027b09..57bbef7 100644 --- a/src/Models/AutoFill.php +++ b/src/Eloquent/AutoFill.php @@ -6,6 +6,14 @@ * methods must follow the "fillValueAttribute". For example, to fill the `foo` attribute, the method * `fillFooAttribute` must exists and return the value needed. Otherwise, try to use $attributes. * + * protected $autoFillable = ['foo']; + * + * protected function fillFooAttribute($value) + * { + * $this->attributes['foo'] = $value; + * } + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,9 +41,10 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\Eloquent; use Illuminate\Support\Str; +use BadMethodCallException; trait AutoFill { @@ -46,19 +55,21 @@ trait AutoFill */ protected function initializeAutoFill() { - foreach ($this->autoFillable() ?? [] as $attribute) { + foreach ($this->autoFillable() as $attribute) { + + if (isset($this->attributes[$attribute])) { + continue; + } + try { $result = $this->{'fill' . Str::studly($attribute) . 'Attribute'}($attribute); - } catch (\BadMethodCallException $exception) { - $method = 'fill' . Str::studly($attribute) . 'Attribute'; - throw new \BadMethodCallException( - "The attribute [$attribute] doesn't have a filler method [$method]." + } catch (BadMethodCallException $exception) { + throw new BadMethodCallException( + "The attribute [$attribute] has no a filler method [fill".Str::studly($attribute)."Attribute]." ); } - if (! isset($this->attributes[$attribute]) && $result) { - $this->attributes[$attribute] = $result; - } + $this->setAttribute($attribute, $result); } } diff --git a/src/Eloquent/Casts/CastEnumerable.php b/src/Eloquent/Casts/CastEnumerable.php new file mode 100644 index 0000000..9a87a05 --- /dev/null +++ b/src/Eloquent/Casts/CastEnumerable.php @@ -0,0 +1,74 @@ + WeatherEnumerable::class, + * ]; + * + * --- + * MIT License + * + * Copyright (c) Italo Israel Baeza Cabrera + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2020 Laravel LLC. + * + * @link https://github.com/DarkGhostHunter/Laratraits + */ + +namespace DarkGhostHunter\Laratraits\Eloquent\Casts; + +use DarkGhostHunter\Laratraits\Enumerable; +use Illuminate\Contracts\Database\Eloquent\CastsAttributes; + +abstract class CastEnumerable extends Enumerable implements CastsAttributes +{ + /** + * @inheritDoc + */ + public function get($model, string $key, $value, array $attributes) + { + return static::as($value); + } + + /** + * @inheritDoc + */ + public function set($model, string $key, $value, array $attributes) + { + return (is_string($value) ? static::as($value) : $value)->current(); + } +} diff --git a/src/DispatchesItself.php b/src/Eloquent/Casts/CastsRepository.php similarity index 51% rename from src/DispatchesItself.php rename to src/Eloquent/Casts/CastsRepository.php index d7a4f21..ced3ea5 100644 --- a/src/DispatchesItself.php +++ b/src/Eloquent/Casts/CastsRepository.php @@ -1,11 +1,19 @@ CastRepository::class, + * ]; + * + * Then, in your code, you can use the Repository methods. + * + * $model->config->get('schedule.days'); + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,38 +41,30 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits; +namespace DarkGhostHunter\Laratraits\Eloquent\Casts; -use Illuminate\Support\Str; +use Illuminate\Config\Repository; +use Illuminate\Contracts\Database\Eloquent\CastsAttributes; -trait DispatchesItself +class CastsRepository implements CastsAttributes { /** - * Dispatches the current instance to a default Job instance. - * - * @return \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Foundation\Bus\PendingChain|mixed + * @inheritDoc */ - public function dispatch() + public function get($model, string $key, $value, array $attributes) { - return $this->defaultJob(...func_get_args()); + return new Repository($value); } /** - * Dispatches this object to a non-default Job. - * - * @param string $job - * @param mixed ...$parameters - * @return \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Foundation\Bus\PendingChain|mixed + * @inheritDoc */ - public function dispatchTo(string $job, ...$parameters) + public function set($model, string $key, $value, array $attributes) { - return $this->{Str::camel($job . 'Job')}(...$parameters); - } + if ($value instanceof Repository) { + return $value->all(); + } - /** - * Creates a Job instance with this object injected to it and some parameters. - * - * @return \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Foundation\Bus\PendingChain|object - */ - abstract protected function defaultJob(); + return (array)$value; + } } diff --git a/src/Models/DefaultColumns.php b/src/Eloquent/DefaultColumns.php similarity index 72% rename from src/Models/DefaultColumns.php rename to src/Eloquent/DefaultColumns.php index 9609895..c07af0a 100644 --- a/src/Models/DefaultColumns.php +++ b/src/Eloquent/DefaultColumns.php @@ -6,6 +6,18 @@ * large chunks of memory when the retrieved record contains too much data that most of the time isn't used, * like walls of text, giant chunks of binary data, raw files encoded as base64 or a large list of columns. * + * Just issue an array of default columns to retrieve and use this trait in your target model. + * + * class Post extends Model + * { + * use DefaultColumns; + * + * protected static $defaultColumns = ['id', 'title', 'excerpt', 'created_at', 'updated_at']; + * + * // ... + * } + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,10 +45,14 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\Eloquent; +use Illuminate\Database\Eloquent\Builder; use DarkGhostHunter\Laratraits\Scopes\DefaultColumns as DefaultColumnsScope; +/** + * @method $this|static|\Illuminate\Database\Eloquent\Builder withoutDefaultColumns() + */ trait DefaultColumns { /** @@ -58,4 +74,15 @@ protected static function getDefaultColumns() { return static::$defaultColumns ?? []; } + + /** + * Makes a query without the Default Columns Scope. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeWithoutDefaultColumns(Builder $builder) + { + return $builder->withoutGlobalScope(DefaultColumnsScope::class); + } } diff --git a/src/Models/HasSlug.php b/src/Eloquent/HasSlug.php similarity index 74% rename from src/Models/HasSlug.php rename to src/Eloquent/HasSlug.php index db388b7..06efd0d 100644 --- a/src/Models/HasSlug.php +++ b/src/Eloquent/HasSlug.php @@ -1,6 +1,6 @@ string('slug')->unique(); * + * To disable routing the model by the slug property, you can disable it using `routeBySlug`: + * + * protected $routeBySlug = false; + * + * The above will use the parent Eloquent Model default routing key. + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -37,10 +43,9 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\Eloquent; use Illuminate\Support\Str; -use Illuminate\Database\Eloquent\Model; trait HasSlug { @@ -51,34 +56,34 @@ trait HasSlug */ protected static function bootHasSlug() { - static::saving(function (Model $model) { - if (! $model->getAttribute('slug')) { - $model->slug = $model->{$model->attributeToSlug()}; + static::saving(function ($model) { + if ($model->isDirty($model->attributeToSlug())) { + $model->setSlug(); } }); } /** - * Initialize the current trait. + * Sets the URL slug from a given string. * * @return void */ - protected function initializeHasSlug() + public function setSlug() { - if (! in_array('slug', $this->fillable, true)) { - $this->fillable[] = 'slug'; - } + $value = $this->getAttribute($this->sluggableAttribute()); + + $this->setAttribute($this->getSlugKey(), $this->slugValue($value)); } /** - * Sets the URL slug from a given string. + * Transforms a given string to a slugged string. * * @param string $value - * @return void + * @return string */ - public function setSlugAttribute(string $value) + protected function slugValue(string $value) { - $this->attributes[$this->getSlugKey()] = Str::slug($value); + return Str::slug($value); } /** @@ -86,9 +91,9 @@ public function setSlugAttribute(string $value) * * @return string */ - public function attributeToSlug() + public function sluggableAttribute() { - return 'name'; + return 'title'; } /** @@ -108,7 +113,6 @@ protected function getSlugKey() */ public function getRouteKeyName() { - return $this->getSlugKey(); + return $this->routeBySlug ?? true ? $this->getSlugKey() : parent::getRouteKeyName(); } - } diff --git a/src/Models/ModelType.php b/src/Eloquent/ModelType.php similarity index 65% rename from src/Models/ModelType.php rename to src/Eloquent/ModelType.php index dec0840..888be7e 100644 --- a/src/Models/ModelType.php +++ b/src/Eloquent/ModelType.php @@ -6,6 +6,41 @@ * this trait you can set a column that will hold the type, while these models extend the same base model * with common properties and methods. This trait will add the scope and the type automatically to each. * + * class Color extends Model + * { + * protected $table = 'colors'; + * + * // .. + * } + * + * class Red extends Color + * { + * use ModelType; + * } + * + * class Blue extends Color + * { + * use ModelType; + * } + * + * When you save "Red" or "Blue", your table would look like this: + * + * | colors | + * | id | type | created_at | updated_at | + * |----|------|------------|------------| + * | 1 | red | 2020-04-01 | 2020-04-01 | + * | 2 | blue | 2020-04-01 | 2020-04-01 | + * + * If you constantly query the column type, is recommended to (at least) create an index for it. + * + * $table->index('type'); + * + * You can also go wild with compound primary keys, if necessary. + * + * $table->dropPrimary('id'); + * $table->primary(['id', 'type']); + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,7 +68,7 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\Eloquent; use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Builder; @@ -50,7 +85,7 @@ protected static function bootModelType() static::addGlobalScope(function (Builder $builder) { $model = $builder->getModel(); - return $builder->where($model->getQualifiedTypeColumn(), $model->getTypeName()); + return $builder->where($model->getModelTypeColumn(), $model->getModelType()); }); } @@ -61,7 +96,7 @@ protected static function bootModelType() */ protected function initializeModelType() { - $this->attributes[$this->getQualifiedTypeColumn()] = $this->getTypeName(); + $this->attributes[$this->getModelTypeColumn()] = $this->getModelType(); } /** @@ -69,7 +104,7 @@ protected function initializeModelType() * * @return string */ - protected function getQualifiedTypeColumn() + public function getModelTypeColumn() { return 'type'; } @@ -79,8 +114,8 @@ protected function getQualifiedTypeColumn() * * @return string */ - protected function getTypeName() + public function getModelType() { - return Str::kebab(class_basename(static::class)); + return Str::snake(class_basename(static::class)); } } diff --git a/src/Models/NeighbourRecords.php b/src/Eloquent/NeighbourRecords.php similarity index 50% rename from src/Models/NeighbourRecords.php rename to src/Eloquent/NeighbourRecords.php index 9f3748f..ebe8717 100644 --- a/src/Models/NeighbourRecords.php +++ b/src/Eloquent/NeighbourRecords.php @@ -6,6 +6,20 @@ * returns the primary key of these neighbour records, but you can override the way the list is queried to * the database, and even if it should be cached or not, which is totally recommended for large tables. * + * You can use it safely like this: + * + * $post = Post::find(4); + * + * $next = $post->nextRecord(); + * $prev = $post->prevRecord(); + * + * if ($next) { + * echo 'The next podcast is ' . $next->id; + * + * By default, it only selects the column used for routing, which is mostly the primary key. You can + * override this in the `queryColumns()` method. + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,7 +47,9 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\Eloquent; + +use Illuminate\Database\Eloquent\Builder; trait NeighbourRecords { @@ -42,43 +58,82 @@ trait NeighbourRecords * * @var array */ - protected $chained; + protected $neighbors; /** * Gets the chained records to this model. * * @return array + */ + protected function getNeighbourRecords() + { + return $this->neighbors ?? $this->neighbors = $this->getRecordsList(); + } + + /** + * Returns the record list. + * + * @return array * @throws \Exception */ - protected function getChainedRecords() + protected function getRecordsList() { - if ($this->chained) { - return $this->chained; - } + return cache() + ->remember("query|{$this->getQualifiedKeyName()}_{$this->getKey()}|neighbours", 60, function () { + return [ + 'next' => $this->queryNextRecord(), + 'prev' => $this->queryPrevRecord(), + ]; + }); + } - $list = $this->getRecordsList(); + /** + * Retrieves the next model. + * + * @return null|static + */ + protected function queryNextRecord() + { + $builder = $this->latest()->where($this->getCreatedAtColumn(), '>', $this->{$this->getCreatedAtColumn()}); - $index = $list->search(function ($item) { - return $item->getKey() === $this->getKey(); - }); + $this->filterNeighbourQuery($builder); - return $this->chained = [ - 'prev' => $list->get($index - 1), - 'next' => $list->get($index + 1), - ]; + return $builder->first($this->queryColumns()); } /** - * Returns the record list. + * Retrieves the previous model. * - * @return \Illuminate\Database\Eloquent\Collection - * @throws \Exception + * @return null|static */ - protected function getRecordsList() + protected function queryPrevRecord() + { + $builder = $this->oldest()->where($this->getCreatedAtColumn(), '<', $this->{$this->getCreatedAtColumn()}); + + $this->filterNeighbourQuery($builder); + + return $builder->first($this->queryColumns()); + } + + /** + * Filter the query. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @return void|\Illuminate\Database\Eloquent\Builder + */ + protected function filterNeighbourQuery(Builder $builder) + { + // + } + + /** + * The columns to query the neighbour records. + * + * @return array|string[] + */ + protected function queryColumns() { - return cache()->remember("query|chapter_{$this->getKey()}|neighbours", 60, function () { - return (new static)->latest()->get([$this->getKeyName()]); - }); + return [$this->getRouteKeyName()]; } /** @@ -89,7 +144,7 @@ protected function getRecordsList() */ public function nextRecord() { - return $this->getChainedRecords()['next']; + return $this->getNeighbourRecords()['next']; } /** @@ -100,6 +155,6 @@ public function nextRecord() */ public function prevRecord() { - return $this->getChainedRecords()['prev']; + return $this->getNeighbourRecords()['prev']; } } diff --git a/src/Models/SoftCachesAccessors.php b/src/Eloquent/SoftCachesAccessors.php similarity index 72% rename from src/Models/SoftCachesAccessors.php rename to src/Eloquent/SoftCachesAccessors.php index 50637f3..f4b52c1 100644 --- a/src/Models/SoftCachesAccessors.php +++ b/src/Eloquent/SoftCachesAccessors.php @@ -3,9 +3,21 @@ * SoftCaches Mutator * * This trait overrides the "mutateAttribute" from the Eloquent Model class and adds a logic to "cache" the - * accessors used into the model instance. You may want to use this to avoid calling the logic every time - * the accessor is used, specially if its logic is costly, like iterating a large set of data or else. + * mutators used into the model instance. You may want to use this to avoid calling the logic every time + * the mutator is used, specially if its logic is costly, like iterating a large set of data or else. * + * For example, let's say you use an mutator that returns a random number. The next time you use it, the + * same number will be returned: + * + * protected function getRandomNumberAttribute() + * { + * return rand(1, 100); + * } + * + * echo $model->random_number; // 63 + * echo $model->random_number; // 63 + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,7 +45,7 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\Eloquent; use Closure; @@ -44,7 +56,7 @@ trait SoftCachesAccessors * * @var array */ - protected $cachedAccessorsValues = []; + protected $cachedMutators = []; /** * Returns the value of an attribute using its cached accessor. @@ -56,11 +68,11 @@ trait SoftCachesAccessors */ protected function mutateAttribute($key, $value) { - if (! in_array($key, $this->cachedAccessors(), true)) { + if (! in_array($key, $this->cachedMutators(), true)) { return parent::mutateAttribute($key, $value); } - return $this->cachedAccessorsValues[$key] = $this->cachedAccessorsValues[$key] + return $this->cachedMutators[$key] = $this->cachedMutators[$key] ?? parent::mutateAttribute($key, $value); } @@ -71,7 +83,7 @@ protected function mutateAttribute($key, $value) */ public function flushAccessorsCache() { - $this->cachedAccessorsValues = []; + $this->cachedMutators = []; return $this; } @@ -93,15 +105,15 @@ public function getAttributeWithoutCache($key) * @param \Closure $callback * @return mixed */ - public function withoutAccessorCache(Closure $callback) + public function withoutMutatorCache(Closure $callback) { - $accessors = $this->cachedAccessorsValues; + $accessors = $this->cachedMutators; $this->flushAccessorsCache(); $value = $callback($this); - $this->cachedAccessorsValues = $accessors; + $this->cachedMutators = $accessors; return $value; } @@ -111,8 +123,8 @@ public function withoutAccessorCache(Closure $callback) * * @return array */ - protected function cachedAccessors() + protected function cachedMutators() { - return $this->cachedAccessors ?? []; + return $this->cachedMutators; } } diff --git a/src/Models/UsesUuid.php b/src/Eloquent/UsesUuid.php similarity index 84% rename from src/Models/UsesUuid.php rename to src/Eloquent/UsesUuid.php index 06113ba..d37ff4f 100644 --- a/src/Models/UsesUuid.php +++ b/src/Eloquent/UsesUuid.php @@ -2,10 +2,10 @@ /** * Throttles UsesUuid * - * This trait auto-fills an autogenerated UUID when the model is instanced. It also adds convenient local - * scopes to the Eloquent Query Builder to find and filter this model through its UUID. For additional - * performance, you can create an index on the UUID column, which can also be changed through here. - * + * This trait auto-fills an autogenerated UUID when the model is instanced. It also adds convenient + * local scopes to the Eloquent Query Builder to find and filter this model through its UUID. For + * additional performance, you can create an index on the UUID column in your model migrations. + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,7 +33,7 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\Eloquent; use Illuminate\Support\Str; use DarkGhostHunter\Laratraits\Scopes\UuidScope; @@ -55,7 +55,7 @@ trait UsesUuid */ protected static function bootUsesUuid() { - if (static::shouldAddGlobalScope()) { + if (static::addUuidGlobalScope()) { static::addGlobalScope(new UuidScope); } } @@ -65,7 +65,7 @@ protected static function bootUsesUuid() * * @return bool */ - protected static function shouldAddGlobalScope() + protected static function addUuidGlobalScope() { return true; } @@ -77,7 +77,7 @@ protected static function shouldAddGlobalScope() */ protected function initializeUsesUuid() { - $this->setAttribute($this->getQualifiedUuidColumn(), $this->generateUuid()); + $this->setAttribute($this->getUuidColumn(), $this->generateUuid()); } /** @@ -85,7 +85,7 @@ protected function initializeUsesUuid() * * @return string */ - public function getQualifiedUuidColumn() + public function getUuidColumn() { return 'uuid'; } diff --git a/src/Enumerable.php b/src/Enumerable.php index fc44bc7..18f064a 100644 --- a/src/Enumerable.php +++ b/src/Enumerable.php @@ -21,6 +21,7 @@ * // ... * } * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -50,7 +51,6 @@ namespace DarkGhostHunter\Laratraits; -use Closure; use Countable; use Traversable; use LogicException; @@ -83,12 +83,12 @@ public function __construct($states = null) $this->states[] = $state; } - if (empty($this->states)) { + if (empty($this->states())) { throw new LogicException('The ' . static::class . ' does not have states to set.'); } if ($this->current) { - $this->set($this->current); + $this->assign($this->current); } } @@ -103,31 +103,13 @@ protected function statesToArray($states) if ($states instanceof Traversable) { $states = iterator_to_array($states); } - - if (is_string($states)) { + elseif (is_string($states)) { return explode(',', $states); } return (array)$states; } - /** - * Returns if one or all states exists. - * - * @param string|array|iterable $state - * @return bool - */ - public function has($state) - { - foreach ($this->statesToArray($state) as $value) { - if (! in_array($value, $this->states, true)) { - return false; - } - } - - return true; - } - /** * Return the current state, or null when uninitialized. * @@ -149,40 +131,36 @@ public function states() } /** - * Sets a state. + * Assigns a state. * * @param string $name * @return $this */ - public function set(string $name) + public function assign(string $name) { // We won't normalize the name here since we can only set one state at a time. We will // hard-check if the state exists in the array, and if it is, we will properly set it. // Otherwise, we'll throw an exception telling the developer this value is incorrect. - if (in_array($name, $this->states)) { + if (in_array($name, $this->states())) { $this->current = $name; return $this; } - throw new LogicException("The state [$name] doesn't exists in this Enumerate instance."); + throw new LogicException("The state '{$name}' doesn't exists in this Enumerate instance."); } /** * Sets an state when a given condition evaluates to true. * - * @param bool|Closure $condition + * @param bool|\Closure $condition * @param string $state * @return $this */ public function when($condition, string $state) { - if ($condition instanceof Closure) { - $condition = $condition(); - } - - if ($condition) { - return $this->set($state); + if (value($condition)) { + return $this->assign($state); } return $this; @@ -197,11 +175,7 @@ public function when($condition, string $state) */ public function unless($condition, string $state) { - if ($condition instanceof Closure) { - $condition = $condition(); - } - - return $this->when(! $condition, $state); + return $this->when(! value($condition), $state); } /** @@ -237,7 +211,7 @@ public function isNot($state) */ public function count() { - return count($this->states); + return count($this->states()); } /** @@ -256,12 +230,11 @@ public function __toString() * @param string $name * @param array $arguments * @return \DarkGhostHunter\Laratraits\Enumerable - * */ public function __call($name, $arguments) { try { - return $this->set($name); + return $this->assign($name); } catch (LogicException $exception) { throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name . '()'); @@ -280,7 +253,7 @@ public static function from($states, string $initial = null) : self $instance = (new static($states)); if ($initial) { - $instance->set($initial); + $instance->assign($initial); } return $instance; @@ -294,6 +267,6 @@ public static function from($states, string $initial = null) : self */ public static function as(string $initial) : self { - return (new static)->set($initial); + return (new static)->assign($initial); } } diff --git a/src/EnumerableStates.php b/src/EnumerableStates.php index e9e5375..8dab4ad 100644 --- a/src/EnumerableStates.php +++ b/src/EnumerableStates.php @@ -4,6 +4,14 @@ * * This traits allows a given class to have one of an strictly enumerated list of states. * + * You can override the Enumerable instance with your own custom class: + * + * protected makeEnumerableInstance() + * { + * return new WeatherEnumerable; + * } + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -31,7 +39,6 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ - namespace DarkGhostHunter\Laratraits; use LogicException; @@ -43,60 +50,74 @@ trait EnumerableStates * * @var \DarkGhostHunter\Laratraits\Enumerable */ - protected $enumerate; + protected $enumerable; /** * Set the state for this current instance. * - * @param $state + * @param string $state * @return $this */ - public function state(string $state = null) + public function assign(string $state) { - $this->getEnumerate()->{$state}(); + $this->getEnumerable()->assign($state); return $this; } /** - * Returns the current state. + * Returns the current state, or null if uninitialized. * - * @return string + * @return string|null */ public function current() { - return $this->getEnumerate()->current(); + return $this->getEnumerable()->current(); } /** * Create an Enumerate instance. * - * @return \DarkGhostHunter\Laratraits\Enumerate|mixed + * @return \DarkGhostHunter\Laratraits\Enumerable|mixed */ - protected function getEnumerate() + public function getEnumerable() { - if ($this->enumerate) { - return $this->enumerate; - } + return $this->enumerable = $this->enumerable ?? $this->makeEnumerableInstance(); + } + /** + * Creates a new Enumerable instance. + * + * @return \DarkGhostHunter\Laratraits\Enumerable + */ + protected function makeEnumerableInstance() + { + return Enumerable::from($this->getEnumerableStates(), $this->getEnumerableInitialState()); + } + + /** + * Return the enumerable states for the current instance. + * + * @return array|iterable + */ + protected function getEnumerableStates() : iterable + { if (defined('static::STATES')) { - $states = static::STATES; - } - elseif (method_exists($this, 'states')) { - $states = $this->states(); - } - else { - $class = static::class; - throw new LogicException("The current {$class} has no states defined."); + return static::STATES; } + throw new LogicException('The current ' . static::class . ' instance has no states defined.'); + } + + /** + * Return the initial state, if any. + * + * @return string|void + */ + protected function getEnumerableInitialState() + { if (defined('static::STATE_INITIAL')) { - $initial = static::STATE_INITIAL; - } - elseif (method_exists($this, 'initial')) { - $initial = $this->initial(); + return static::STATE_INITIAL; } - - return $this->enumerate = Enumerable::from($states, $initial ?? null); } } diff --git a/src/DiscoverClasses.php b/src/FiresItself.php similarity index 55% rename from src/DiscoverClasses.php rename to src/FiresItself.php index 3cd4221..761b57c 100644 --- a/src/DiscoverClasses.php +++ b/src/FiresItself.php @@ -1,11 +1,15 @@ path($path); - - if ($methodOrInterface) { - if (interface_exists($methodOrInterface)) { - $discoverer->filterByInterface($methodOrInterface); - } - else { - $discoverer->filterByMethod($methodOrInterface); - } - } + return app('events')->dispatch(static::class, new static(...$args)); + } - return $discoverer->discover(); + /** + * Fires this event and halts + * + * @param mixed ...$args + * @return null|array + */ + public static function fireHalted(...$args) + { + return app('events')->dispatch(static::class, new static(...$args), true); } } diff --git a/src/InfiniteHigherOrderTapProxy.php b/src/InfiniteHigherOrderTapProxy.php index c6ba7bd..ccfce8e 100644 --- a/src/InfiniteHigherOrderTapProxy.php +++ b/src/InfiniteHigherOrderTapProxy.php @@ -3,7 +3,7 @@ * Infinite Higher Order Tap Proxy * * This class extends the original "Tapable" class but changes the call to always return this object. - * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera diff --git a/src/Jobs/DispatchablePipeline.php b/src/Jobs/DispatchablePipeline.php index cb718eb..63c0462 100644 --- a/src/Jobs/DispatchablePipeline.php +++ b/src/Jobs/DispatchablePipeline.php @@ -6,6 +6,7 @@ * this Job is dispatched. This Job instance is used when you add the "DispatchesPipeline" trait in your * custom pipelines. Like all Jobs in Laravel, this is will return void once handled by the dispatcher. * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera diff --git a/src/Middleware/CacheStaticResponse.php b/src/Middleware/CacheStaticResponse.php index b691693..1103e5a 100644 --- a/src/Middleware/CacheStaticResponse.php +++ b/src/Middleware/CacheStaticResponse.php @@ -4,6 +4,16 @@ * * Caches a (hopefully static) Response for the given minutes, and other store apart of the default. * + * Route::get('post', 'PostController@show') + * ->middleware('DarkGhostHunter\Laratraits\Middleware\CacheStaticResponse'); + * + * Alternatively, you can register an alias in your HTTP Kernel for this middleware, making it easy + * to add options like the time-to-live for the cached response, and which cache store to use. + * + * Route::get('post', 'PostController@show') + * ->middleware('cache.static:1440,redis'); + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera diff --git a/src/Middleware/ShareAuthenticatedUser.php b/src/Middleware/ShareAuthenticatedUser.php index 160b519..297cc28 100644 --- a/src/Middleware/ShareAuthenticatedUser.php +++ b/src/Middleware/ShareAuthenticatedUser.php @@ -2,8 +2,27 @@ /** * Share Authenticated User Middleware * - * This allows to share in all your views the "authenticated" variable containing the authenticated user, if any. + * This allows to share in all your views the "authenticated" variable containing the + * authenticated user, if any. * + * It's recommended to add it in your HTTP Kernel as part of the 'web' stack of middleware: + * + * protected $middlewareGroups = [ + * 'web' => [ + * // ... + * + * \DarkGhostHunter\Laratraits\Middleware\ShareAuthenticatedUser::class + * ]; + * ]; + * + * Then, in your views, you can use the "$authenticated" variable anywhere, which can be "null" + * if not authenticated. + * + * @auth + *

Welcome, {{ $authenticated->name }}

+ * @endauth + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera diff --git a/src/Middleware/ValidateConsumableSignature.php b/src/Middleware/ValidateConsumableSignature.php index 9c0edea..f9bab74 100644 --- a/src/Middleware/ValidateConsumableSignature.php +++ b/src/Middleware/ValidateConsumableSignature.php @@ -4,6 +4,9 @@ * * Makes the signed request valid only for one time, except on client (4xx) or server errors (5xx). * + * You can set + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -69,24 +72,17 @@ public function __construct(Repository $cache) public function handle($request, Closure $next) { if ($this->signatureNotConsumed($request) && $request->hasValidSignature()) { - return $next($request); - } - throw new InvalidSignatureException; - } + $response = $next($request); - /** - * Handle the sent response. - * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Http\Response $response - * @return void - */ - public function terminate($request, $response) - { - if (! $response->isServerError() && ! $response->isClientError()) { - $this->consumeSignature($request); + if (! $response->isServerError() && ! $response->isClientError()) { + $this->consumeSignature($request); + } + + return $response; } + + throw new InvalidSignatureException; } /** @@ -114,7 +110,7 @@ protected function consumeSignature(Request $request) } /** - * Return the cache Key to check + * Return the key to check in the cache for the request. * * @param \Illuminate\Http\Request $request * @return string diff --git a/src/Models/HasFile.php b/src/Models/HasFile.php deleted file mode 100644 index 7b27366..0000000 --- a/src/Models/HasFile.php +++ /dev/null @@ -1,220 +0,0 @@ -saveFileContents(); - }); - } - - /** - * Returns the column that holds the hash of the file. - * - * @return string - */ - protected function getQualifiedFileHashColumn() - { - return 'file_hash'; - } - - /** - * Creates a hash from the file contents for comparison. - * - * @param string $value - * @return string - */ - protected function createFileHash(string $value) - { - return sha1($value); - } - - /** - * Saves the File contents to the storage disk. - * - * @param string|null $content - * @param string|null $path - * @return bool - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException - */ - public function saveFileContents(string $content = null, string $path = null) - { - if ($content) { - $this->setFileContents($content); - } - - if (! $this->file_contents) { - return false; - } - - $hash = $this->createFileHash($this->file_contents); - - $hashColumn = $this->getQualifiedFileHashColumn(); - - // We will first check if the file has changed in someway. When there is no changes, we will - // just return true since there is no need to save again the same content again. Otherwise - // we will save it and also persist the hash of the file to compare if it changed later. - if (! $this->isFileChanged($hash)) { - return true; - } - - if (! $path) { - $path = $this->getFilePath(); - } else { - $this->setAttribute($this->getQualifiedStoragePathColumn(), $path); - } - - if ($this->defaultStorage()->put($path, $this->file_contents)) { - $this->setAttribute($hashColumn, $hash); - return true; - } - - return false; - } - - /** - * Returns the File Contents property. - * - * @return string - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException - */ - public function getFileContents() - { - if ($this->file_contents) { - return $this->file_contents; - } - - return $this->file_contents = $this->defaultStorage()->get($this->getFilePath()); - } - - /** - * Returns if the file has Changed. Optionally, compared with a given hash. - * - * @param string|null $hash - * @return bool - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException - */ - public function isFileChanged(string $hash = null) - { - $hash = $hash ?? $this->createFileHash($this->getFileContents()); - - return $hash !== $this->getAttribute($this->getQualifiedFileHashColumn()); - } - - /** - * Sets the File content property, and returns the hash of the content. - * - * @param string $value - * @return string - */ - public function setFileContents(string $value) - { - return $this->createFileHash($this->file_contents = $value); - } - - /** - * The Storage disk to use. If its not set, the default will be used. - * - * @return \Illuminate\Contracts\Filesystem\Filesystem - */ - protected function defaultStorage() - { - $name = $this->getQualifiedStorageDiskColumn(); - - if (! $disk = $this->getAttribute($name)) { - $this->setAttribute($name, $disk = config('filesystems.default')); - } - - return Storage::disk($disk); - } - - /** - * Returns the column that holds the disk to use - * - * @return null|string - */ - protected function getQualifiedStorageDiskColumn() - { - return 'disk'; - } - - /** - * The path name to use to locate the file in the disk. - * - * @return string - */ - protected function getQualifiedStoragePathColumn() - { - return 'path'; - } - - /** - * Returns the File path. - * - * @return string - */ - protected function getFilePath() - { - if ($path = $this->getAttribute($this->getQualifiedStoragePathColumn())) { - return $path; - } - - throw new \LogicException('There is no file path set to get/put the file contents.'); - } -} diff --git a/src/Multitaps.php b/src/Multitaps.php index 415ee43..4807649 100644 --- a/src/Multitaps.php +++ b/src/Multitaps.php @@ -5,7 +5,7 @@ * This is a hacky way to "tap" indefinitely the current class instance. To stop multitaping, you can use * the "$target" public property to access the underlying object, or append "AndUntap" to any method. * This also allows for an optional callable to be used when starting the multitaping in here. - * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera diff --git a/src/PipesThrough.php b/src/PipesThrough.php index 0da44d0..1b23a56 100644 --- a/src/PipesThrough.php +++ b/src/PipesThrough.php @@ -6,6 +6,13 @@ * the Service Container, and returns a result (hopefully the same instance). You can change the pipes, * the Closure to receive the result, the default methods to use, and even the Pipeline class itself. * + * // Pipe the class immediately. + * $result = $class->pipe(); + * + * // Pipe the class asynchronously. + * $class->dispatchPipeline(); + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -54,7 +61,7 @@ public function pipe($pipes = null, Closure $destination = null) $pipeline = $this->makePipeline()->send($this); if ($pipes) { - $pipeline->through($pipes); + $pipeline->through((array)$pipes); } return $destination @@ -72,20 +79,24 @@ protected function makePipeline() : PipelineContract // By default we create the default Pipeline class, but if your pipes don't depend // on a Service Container, you can just instance the pipeline with an empty one. // If you need custom pipeline handling, you can extend the default pipeline. - return app(Pipeline::class); + // + // return new Pipeline(new \Illuminate\Container\Container); + + return app($this->pipeline ?? Pipeline::class); } /** * Queues the pipeline to a Job. * + * @param mixed|string[] ...$pipes * @return \Illuminate\Foundation\Bus\PendingDispatch */ - public function dispatchPipeline() + public function dispatchPipeline(...$pipes) { $pipeline = $this->makePipeline(); - if (func_num_args()) { - $pipeline->through(...func_get_args()); + if (! empty($pipes)) { + $pipeline->through($pipes); } return DispatchablePipeline::dispatch($pipeline, $this); diff --git a/src/RendersFromMarkdown.php b/src/RendersFromMarkdown.php index 798a1f8..4443b53 100644 --- a/src/RendersFromMarkdown.php +++ b/src/RendersFromMarkdown.php @@ -7,7 +7,7 @@ * fail if the text to parse is empty; you should validate the data of the class beforehand. * * @see \Illuminate\Contracts\Support\Htmlable - * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -39,7 +39,6 @@ use Illuminate\Mail\Markdown; use Illuminate\Support\Collection; -use Illuminate\Support\HtmlString; use const PHP_EOL; trait RendersFromMarkdown @@ -67,7 +66,7 @@ public function __toString() /** * Returns the markdown text to parse. * - * @return string|mixed + * @return string|array|string[]|\Illuminate\Support\Stringable|\Illuminate\Support\Stringable[] */ abstract protected function getMarkdown(); @@ -78,9 +77,7 @@ abstract protected function getMarkdown(); */ public function parseMarkdown() { - if (empty($text = $this->getMarkdown())) { - return new HtmlString(''); - } + $text = $this->getMarkdown(); // If the data is an array, or a collection, we will treat each array item as a line. if ($text instanceof Collection) { diff --git a/src/SavesToCache.php b/src/SavesToCache.php index e614f7a..4eb5ca2 100644 --- a/src/SavesToCache.php +++ b/src/SavesToCache.php @@ -3,7 +3,7 @@ * Saves to Cache * * This trait allows an object to be saved to the cache. - * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -34,11 +34,7 @@ namespace DarkGhostHunter\Laratraits; use LogicException; -use JsonSerializable; -use Illuminate\Support\Facades\Cache; use Illuminate\Contracts\Cache\Repository; -use Illuminate\Contracts\Support\Jsonable; -use Illuminate\Contracts\Support\Htmlable; trait SavesToCache { @@ -61,7 +57,7 @@ public function saveToCache(string $key = null, $ttl = 60) */ protected function defaultCache() : Repository { - return Cache::store(); + return cache()->store(); } /** @@ -69,34 +65,18 @@ protected function defaultCache() : Repository * * @return string */ - protected function defaultCacheKey() + protected function defaultCacheKey() : string { - throw new LogicException('The class ' . class_basename($this) . ' has no default cache key.'); + throw new LogicException('The class ' . get_class($this) . ' has no default cache key.'); } /** * The value to insert into the cache. * - * @return string|$this + * @return $this */ protected function toCache() { - if ($this instanceof Jsonable) { - return $this->toJson(); - } - - if ($this instanceof JsonSerializable) { - return json_encode($this); - } - - if ($this instanceof Htmlable) { - return $this->toHtml(); - } - - if (method_exists($this, '__toString')) { - return $this->__toString(); - } - return $this; } } diff --git a/src/SavesToSession.php b/src/SavesToSession.php index ae2e88b..1833ac1 100644 --- a/src/SavesToSession.php +++ b/src/SavesToSession.php @@ -4,6 +4,7 @@ * * This trait allows an object to be saved into the session. * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -34,10 +35,6 @@ namespace DarkGhostHunter\Laratraits; use LogicException; -use JsonSerializable; -use Illuminate\Contracts\Session\Session; -use Illuminate\Contracts\Support\Jsonable; -use Illuminate\Contracts\Support\Htmlable; trait SavesToSession { @@ -49,7 +46,7 @@ trait SavesToSession */ public function saveToSession(string $key = null) { - app(Session::class)->put($key ?? $this->defaultSessionKey(), $this->toSession()); + session()->put($key ?? $this->defaultSessionKey(), $this->toSession()); } /** @@ -57,34 +54,18 @@ public function saveToSession(string $key = null) * * @return string */ - protected function defaultSessionKey() + protected function defaultSessionKey() : string { - throw new LogicException('The class ' . class_basename($this) . ' has no default session key.'); + throw new LogicException('The class ' . get_class($this) . ' has no default session key.'); } /** * The value to insert into the Session. * - * @return string|$this + * @return $this */ protected function toSession() { - if ($this instanceof Jsonable) { - return $this->toJson(); - } - - if ($this instanceof JsonSerializable) { - return json_encode($this); - } - - if ($this instanceof Htmlable) { - return $this->toHtml(); - } - - if (method_exists($this, '__toString')) { - return $this->__toString(); - } - return $this; } } diff --git a/src/SavesToStorage.php b/src/SavesToStorage.php index 67c47b0..802626b 100644 --- a/src/SavesToStorage.php +++ b/src/SavesToStorage.php @@ -4,6 +4,7 @@ * * This trait allows an object to be saved to the application storage. * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -34,10 +35,7 @@ namespace DarkGhostHunter\Laratraits; use LogicException; -use JsonSerializable; use Illuminate\Support\Facades\Storage; -use Illuminate\Contracts\Support\Jsonable; -use Illuminate\Contracts\Support\Htmlable; trait SavesToStorage { @@ -68,32 +66,16 @@ protected function defaultStorage() */ protected function defaultStoragePath() : string { - throw new LogicException('The class ' . class_basename($this) . ' has no default storage path.'); + throw new LogicException('The class ' . get_class($this) . ' has no default path to store.'); } /** * Get content that should be persisted into the Storage. * - * @return string|$this + * @return $this */ protected function toStore() { - if ($this instanceof Jsonable) { - return $this->toJson(); - } - - if ($this instanceof JsonSerializable) { - return json_encode($this); - } - - if ($this instanceof Htmlable) { - return $this->toHtml(); - } - - if (method_exists($this, '__toString')) { - return $this->__toString(); - } - return $this; } } diff --git a/src/Scopes/DefaultColumns.php b/src/Scopes/DefaultColumns.php index 50ac5cb..560fbb1 100644 --- a/src/Scopes/DefaultColumns.php +++ b/src/Scopes/DefaultColumns.php @@ -5,7 +5,7 @@ * This scopes injects a "select" into the Eloquent Query Builder with a given list of columns, which can be later * be overridden by the query itself. This allows the queried record to only select some columns instead of all, * which without can become problematic when tidying up memory consumption and data retrieved for each Model. - * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -63,10 +63,10 @@ public function __construct(array $defaultColumns) */ public function apply(Builder $builder, Model $model) { - if ($this->defaultColumns === [] || $builder->getQuery()->columns) { - return $builder; + if ($this->defaultColumns === [] && empty($builder->getQuery()->columns)) { + return $builder->select($this->defaultColumns); } - return $builder->select($this->defaultColumns); + return $builder; } } diff --git a/src/Scopes/MacrosEloquent.php b/src/Scopes/MacrosEloquent.php index 8e36133..10cf9ea 100644 --- a/src/Scopes/MacrosEloquent.php +++ b/src/Scopes/MacrosEloquent.php @@ -5,7 +5,7 @@ * This trait allows you to extend the Eloquent Builder instance using local macros, which are macros but * only valid for the instance itself instance of globally. This cycles through all the scope methods, * filters only those that starts with "macro", executes them receiving a Closure, and adds them. - * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -35,6 +35,8 @@ namespace DarkGhostHunter\Laratraits\Scopes; +use ReflectionClass; +use ReflectionMethod; use Illuminate\Database\Eloquent\Builder; trait MacrosEloquent @@ -44,15 +46,19 @@ trait MacrosEloquent * * @param \Illuminate\Database\Eloquent\Builder $builder * @return void + * @throws \ReflectionException */ public function extend(Builder $builder) { - // We will cycle through all the present methods in the present Scope instance and - // add macros for only the methods who start with "macro" that return a Closure. - // For example, the "macroAddOne()" method will be registered as "addOne()". - foreach (get_class_methods($this) as $method) { - if (strpos($method, 'macro') === 0) { - $builder->macro(lcfirst(substr($method, 5)), $this->{$method}($builder)); + // We will cycle through all the public static methods in the present Scope instance + // and add macros to the Builder instance by filtering those starting with "macro". + // To say an example, the "macroAddOne()" method will be registered as "addOne()". + $methods = (new ReflectionClass($this)) + ->getMethods(ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_STATIC); + + foreach ($methods as $method) { + if (strpos($method->getName(), 'macro') === 0) { + $builder->macro(lcfirst(substr($method, 5)), static::class . '::' . $method); } } } diff --git a/src/Scopes/UuidScope.php b/src/Scopes/UuidScope.php index 40fcffb..abbaef8 100644 --- a/src/Scopes/UuidScope.php +++ b/src/Scopes/UuidScope.php @@ -5,7 +5,7 @@ * This conveniently adds local scopes to handle UUIDs to the Eloquent Query Builder. These scopes are only * valid for the Builder instance itself, and doesn't interfere with other builders of other models. You * can register this Scope all by yourself, but it's better to use the UsesUuid trait in your models. - * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -50,121 +50,126 @@ class UuidScope implements Scope */ public function apply(Builder $builder, Model $model) { - // Nothing to add, really. + // } /** * Return a Closure that find a model by its UUID. * - * @return \Closure + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param string|array|\Illuminate\Contracts\Support\Arrayable $uuid + * @param string[] $columns + * @return \Illuminate\Database\Eloquent\Builder */ - protected function macroFindUuid() + public static function macroFindUuid(Builder $builder, $uuid, $columns = ['*']) { - return function(Builder $builder, $uuid, $columns = ['*']) { - if (is_array($uuid) || $uuid instanceof Arrayable) { - return $builder->findManyUuid($uuid, $columns); - } + if (is_array($uuid) || $uuid instanceof Arrayable) { + return $builder->findManyUuid($uuid, $columns); + } - return $builder->whereUuid($uuid)->first($columns); - }; + return $builder->whereUuid($uuid)->first($columns); } /** * Return a Closure that find multiple models by their UUID. * - * @return \Closure + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param string|array|\Illuminate\Contracts\Support\Arrayable $uuids + * @param string[] $columns + * @return \Illuminate\Database\Eloquent\Builder */ - protected function macroFindManyUuid() + public static function macroFindManyUuid(Builder $builder, $uuids, $columns = ['*']) { - return function(Builder $builder, $uuids, $columns = ['*']) { - $uuids = $uuids instanceof Arrayable ? $uuids->toArray() : $uuids; + $uuids = $uuids instanceof Arrayable ? $uuids->toArray() : $uuids; - if (empty($uuids)) { - return $builder->getModel()->newCollection(); - } + if (empty($uuids)) { + return $builder->getModel()->newCollection(); + } - return $builder->whereUuid($uuids)->get($columns); - }; + return $builder->whereUuid($uuids)->get($columns); } /** * Return a Closure that find a model by its UUID or throw an exception. * - * @return \Closure + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param string|array|\Illuminate\Contracts\Support\Arrayable $uuid + * @param string[] $columns + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model[] */ - protected function macroFindUuidOrFail() + public static function macroFindUuidOrFail(Builder $builder, $uuid, $columns = ['*']) { - return function(Builder $builder, $uuid, $columns = ['*']) { - $result = $builder->findUuid($uuid, $columns); - - if (is_array($uuid)) { - if (count($result) === count(array_unique($uuid))) { - return $result; - } - } elseif ($result !== null) { + $result = $builder->findUuid($uuid, $columns); + + if (is_array($uuid)) { + if (count($result) === count(array_unique($uuid))) { return $result; } - - throw (new ModelNotFoundException)->setModel( - get_class($builder->getModel()), $uuid - ); - }; + } + elseif ($result !== null) { + return $result; + } + + throw (new ModelNotFoundException)->setModel( + get_class($builder->getModel()), $uuid + ); } /** * Return a Closure that find a model by its UUID or return fresh model instance. * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param mixed $uuid + * @param string[] $columns * @return \Closure */ - protected function macroFindUuidOrNew() + public static function macroFindUuidOrNew(Builder $builder, $uuid, $columns = ['*']) { - return function(Builder $builder, $uuid, $columns = ['*']) { - if (($model = $builder->findUuid($uuid, $columns)) !== null) { - return $model; - } + if (($model = $builder->findUuid($uuid, $columns)) !== null) { + return $model; + } - return $builder->newModelInstance(); - }; + return $builder->newModelInstance(); } /** * Return a Closure that adds a where clause on the UUID column to the query. * - * @return \Closure + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param string|array|\Illuminate\Contracts\Support\Arrayable $uuid + * @return \Illuminate\Database\Eloquent\Builder */ - protected function macroWhereUuid() + public static function macroWhereUuid(Builder $builder, $uuid) { - return function(Builder $builder, $uuid) { - if (is_array($uuid) || $uuid instanceof Arrayable) { - $builder->getQuery()->whereIn( - $builder->getModel()->getQualifiedUuidColumn(), $uuid - ); + if (is_array($uuid) || $uuid instanceof Arrayable) { + $builder->getQuery()->whereIn( + $builder->getModel()->getUuidColumn(), $uuid + ); - return $builder; - } + return $builder; + } - return $builder->where($builder->getModel()->getQualifiedUuidColumn(), '=', $uuid); - }; + return $builder->where($builder->getModel()->getUuidColumn(), '=', $uuid); } /** * Return a Closure that add a where clause on the primary key to the query. * - * @return \Closure + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param string|array|\Illuminate\Contracts\Support\Arrayable $uuid + * @return \Illuminate\Database\Eloquent\Builder */ - protected function macroWhereUuidNot() + public static function macroWhereUuidNot(Builder $builder, $uuid) { - return function (Builder $builder, $uuid) { - if (is_array($uuid) || $uuid instanceof Arrayable) { - $builder->getQuery()->whereNotIn( - $builder->getModel()->getQualifiedUuidColumn(), $uuid - ); + if (is_array($uuid) || $uuid instanceof Arrayable) { + $builder->getQuery()->whereNotIn( + $builder->getModel()->getUuidColumn(), $uuid + ); - return $builder; - } + return $builder; + } - return $builder->where($builder->getModel()->getQualifiedUuidColumn(), '!=', $uuid); - }; + return $builder->where($builder->getModel()->getUuidColumn(), '!=', $uuid); } } diff --git a/src/SendsToHttp.php b/src/SendsToHttp.php new file mode 100644 index 0000000..e063202 --- /dev/null +++ b/src/SendsToHttp.php @@ -0,0 +1,101 @@ +send(); + * + * --- + * MIT License + * + * Copyright (c) Italo Israel Baeza Cabrera + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2020 Laravel LLC. + * + * @link https://github.com/DarkGhostHunter/Laratraits + */ + +namespace DarkGhostHunter\Laratraits; + +use LogicException; +use Illuminate\Http\Client\Factory; +use Illuminate\Http\Client\PendingRequest; + +trait SendsToHttp +{ + /** + * Sends the current instance via HTTP. + * + * @param string|null $url + * @return \Illuminate\Http\Client\Response + */ + public function send(string $url = null) + { + return $this->performHttpRequest( + $this->createPendingRequest(), $url ?? $this->url() + ); + } + + /** + * Sends the current instance via HTTP. + * + * @param \Illuminate\Http\Client\PendingRequest $request + * @param string $url + * @return \Illuminate\Http\Client\Response + * @see \Illuminate\Http\Client\PendingRequest + */ + protected function performHttpRequest(PendingRequest $request, string $url) + { + return $request->asJson()->post($url, $this->toHttp()); + } + + /** + * Prepares the HTTP Request to be sent. + * + * @return \Illuminate\Http\Client\PendingRequest + */ + protected function createPendingRequest() + { + return new PendingRequest(new Factory); + } + + /** + * Return the HTTP endpoint to send this object. + * + * @return string + */ + protected function url() : string + { + if (property_exists($this, 'url')) { + return $this->url; + } + + throw new LogicException('This instance of ' . static::class . ' has no default URL to be sent to.'); + } + + /** + * Get content that should be sent inside the Request. + * + * @return array|string|mixed + */ + abstract protected function toHttp(); +} diff --git a/src/ServiceProviders/RegisterBladeExtensions.php b/src/ServiceProviders/RegisterBladeExtensions.php new file mode 100644 index 0000000..7cda19c --- /dev/null +++ b/src/ServiceProviders/RegisterBladeExtensions.php @@ -0,0 +1,87 @@ + 'App\Blade\Directives\AlertComponent', + * 'status-now' => 'App\Blade\Directives\Status@now', + * ] + * + * protected $if = [ + * 'cloud' => 'App\Blade\Conditions\Cloud', + * 'local' => 'App\Blade\Conditions\Local@condition', + * ] + * + * protected $include = [ + * 'input' => 'includes.input' + * 'authavatar' => 'includes.auth_avatar' + * ] + * + * public function boot() + * { + * $this->registerBladeExtensions(); + * + * // ... + * } + * + * @see https://laravel.com/docs/blade#extending-blade + * + * --- + * MIT License + * + * Copyright (c) Italo Israel Baeza Cabrera + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2020 Laravel LLC. + * + * @link https://github.com/DarkGhostHunter/Laratraits + */ + +namespace DarkGhostHunter\Laratraits\ServiceProviders; + +use Illuminate\Support\Facades\Blade; + +trait RegisterBladeExtensions +{ + /** + * Register directives, includes and if statements in Blade. + * + * @return void + */ + protected function registerBladeExtensions() + { + /** @var \Illuminate\View\Compilers\BladeCompiler $compiler */ + $compiler = Blade::getFacadeRoot(); + + foreach ($this->directives ?? [] as $name => $handler) { + $compiler->directive($name, $handler); + } + + foreach ($this->if ?? [] as $name => $handler) { + $compiler->if($name, $handler); + } + + foreach ($this->include ?? [] as $name => $handler) { + $compiler->include($name, $handler); + } + } +} diff --git a/src/Models/DynamicallyMutates.php b/src/ServiceProviders/RegisterGates.php similarity index 54% rename from src/Models/DynamicallyMutates.php rename to src/ServiceProviders/RegisterGates.php index f1642c4..046d52b 100644 --- a/src/Models/DynamicallyMutates.php +++ b/src/ServiceProviders/RegisterGates.php @@ -1,11 +1,24 @@ 'App\Auth\Gates\Admin@viewDashboard', + * 'create-users' => 'App\Auth\Gates\Admin@createUsers', + * ]; + * + * public function boot() + * { + * $this->registerPolicies(); + * + * $this->registerGates(); + * + * // ... + * } + * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -33,32 +46,24 @@ * @link https://github.com/DarkGhostHunter/Laratraits */ -namespace DarkGhostHunter\Laratraits\Models; +namespace DarkGhostHunter\Laratraits\ServiceProviders; + +use Illuminate\Support\Facades\Gate; -trait DynamicallyMutates +trait RegisterGates { /** - * Dynamically mutates an attribute by the other attribute value as "type". + * Registers gates. * - * @param string $value The attribute name to take. - * @param string $type The attribute that holds the type - * @return mixed + * @return void */ - protected function castAttributeInto(string $value, string $type = null) + public function registerGates() { - $type = $type ?? $value . '_type'; - - // We will save the original casted attributes, swap them, and then restore them. - $original = $this->casts; - - $this->casts = [ - $value => $this->attributes[$type], - ]; - - $attribute = $this->castAttribute($value, $this->attributes[$value]); - - $this->casts = $original; + /** @var \Illuminate\Contracts\Auth\Access\Gate $gate */ + $gate = Gate::getFacadeRoot(); - return $attribute; + foreach ($this->gates ?? [] as $action => $handler) { + $gate->define($action, $handler); + } } } diff --git a/src/ServiceProviders/RegistersObservers.php b/src/ServiceProviders/RegistersObservers.php new file mode 100644 index 0000000..97704fe --- /dev/null +++ b/src/ServiceProviders/RegistersObservers.php @@ -0,0 +1,65 @@ + 'App\Observers\UserObserver', + * 'App\Post' => ['App\Observers\PublicationObserver', 'App\Observers\HomeObserver'] + * ] + * + * public function boot() + * { + * $this->registerObservers(); + * + * // ... + * } + * + * --- + * MIT License + * + * Copyright (c) Italo Israel Baeza Cabrera + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2020 Laravel LLC. + * + * @link https://github.com/DarkGhostHunter/Laratraits + */ + +namespace DarkGhostHunter\Laratraits\ServiceProviders; + +trait RegistersObservers +{ + /** + * Registers an array of handlers for a Eloquent event. + * + * @return void + */ + protected function registerObservers() + { + foreach ($this->observers ?? [] as $model => $handlers) { + foreach ((array)$handlers as $handler) { + $model::observe($handler); + } + } + } +} diff --git a/src/ValidatesItself.php b/src/ValidatesItself.php index 2ced186..65c56eb 100644 --- a/src/ValidatesItself.php +++ b/src/ValidatesItself.php @@ -6,6 +6,7 @@ * issue your own data to be validated, but preferably you should let the class point the data * itself. You can use a Closure to execute an "after callback" in the Validator instance. * + * --- * MIT License * * Copyright (c) Italo Israel Baeza Cabrera @@ -37,7 +38,6 @@ namespace DarkGhostHunter\Laratraits; use LogicException; -use Illuminate\Contracts\Validation\Factory; trait ValidatesItself { @@ -57,7 +57,7 @@ trait ValidatesItself */ public function validator(array $data = null, $after = null) { - $validator = app(Factory::class)->make( + $validator = validator( $data ?? $this->validationData(), $this->validationRules(), $this->validationMessages(), @@ -97,7 +97,6 @@ public function validates(array $data = null, $after = null) * @param null|array $data * @param null|callable|string $after * @return array - * @throws \Illuminate\Validation\ValidationException */ public function validate(array $data = null, $after = null) { @@ -119,7 +118,7 @@ public function validated() * * @return array */ - public function validationData() + public function validationData() : array { throw new LogicException('The class ' . class_basename($this) . ' has no default data to validate.'); } diff --git a/tests/DiscoverClassesTest.php b/tests/DiscoverClassesTest.php deleted file mode 100644 index d5d88e3..0000000 --- a/tests/DiscoverClassesTest.php +++ /dev/null @@ -1,70 +0,0 @@ -markTestSkipped('Github actions does not detect the stub directory'); - } - - $discovers = new class() { - use DiscoverClasses; - }; - - $app = Mockery::spy(Application::class); - $app->shouldReceive('path') - ->andReturn(realpath(__DIR__)); - $app->shouldReceive('basePath') - ->andReturn(realpath(substr(__DIR__, 0, strrpos(__DIR__, DS)))); - - $this->app->when(ClassDiscoverer::class) - ->needs(Application::class) - ->give(function () use ($app) { - return $app; - }); - - /** @var \Illuminate\Support\Collection $discovered */ - $discovered = $discovers->discover('tests/Stubs'); - - $this->assertCount(3, $discovered); - $this->assertTrue($discovered->contains('Tests\Stubs\TestDiscoverableClassFoo')); - $this->assertTrue($discovered->contains('Tests\Stubs\TestDiscoverableClassBar')); - $this->assertTrue($discovered->contains('Tests\Stubs\TestDirectory\TestDiscoverableClassQuz')); - - $discovered = $discovers->discover('Tests/Stubs', 'foo'); - - $this->assertCount(2, $discovered); - $this->assertTrue($discovered->contains('Tests\Stubs\TestDiscoverableClassFoo')); - $this->assertTrue($discovered->contains('Tests\Stubs\TestDirectory\TestDiscoverableClassQuz')); - - $discovered = $discovers->discover('Tests/Stubs', 'Tests\Stubs\TestInterface'); - - $this->assertCount(2, $discovered); - $this->assertTrue($discovered->contains('Tests\Stubs\TestDiscoverableClassFoo')); - $this->assertTrue($discovered->contains('Tests\Stubs\TestDirectory\TestDiscoverableClassQuz')); - } - - public function testExceptionWhenInterfaceDoesntExists() - { - if (getenv('GITHUB_ACTIONS')) { - return $this->markTestSkipped('Github actions does not detect the stub directory'); - } - - $this->expectException(InvalidArgumentException::class); - $discoverer = app(ClassDiscoverer::class)->path('tests'); - - $discoverer->filterByInterface('invalid_interface'); - } - -} diff --git a/tests/DispatchesItselfTest.php b/tests/DispatchesItselfTest.php deleted file mode 100644 index 241b062..0000000 --- a/tests/DispatchesItselfTest.php +++ /dev/null @@ -1,45 +0,0 @@ -mock(QueuedCommand::class); - - $dispatchable = new class() { - use DispatchesItself; - - protected function testJob(...$parameters) - { - return QueuedCommand::dispatchNow('test_job', $parameters); - } - - protected function defaultJob(...$parameters) - { - return QueuedCommand::dispatchNow(...$parameters); - } - }; - - $job->shouldReceive('dispatch') - ->with(['foo', 'bar']) - ->andReturnSelf(); - - $job->shouldReceive('dispatch') - ->with('test_job', ['foo', 'bar']) - ->andReturnSelf(); - - $dispatchable->dispatch('foo', 'bar'); - $dispatchable->dispatchTo('test', 'foo', 'bar'); - - $bus->assertDispatched(QueuedCommand::class); - } -} diff --git a/tests/EnumerableStatesTest.php b/tests/EnumerableStatesTest.php index 9838826..9e30fc1 100644 --- a/tests/EnumerableStatesTest.php +++ b/tests/EnumerableStatesTest.php @@ -19,7 +19,7 @@ protected function states() }; $this->assertNull($class->current()); - $this->assertInstanceOf(get_class($class), $class->state('foo')); + $this->assertInstanceOf(get_class($class), $class->assign('foo')); $this->assertEquals('foo', $class->current()); } @@ -32,7 +32,7 @@ public function test_has_states_with_const() }; $this->assertNull($class->current()); - $this->assertInstanceOf(get_class($class), $class->state('foo')); + $this->assertInstanceOf(get_class($class), $class->assign('foo')); $this->assertEquals('foo', $class->current()); } @@ -46,7 +46,7 @@ public function test_exception_when_invalid_state() protected const STATES = ['foo', 'bar']; }; - $class->state('invalid'); + $class->assign('invalid'); } public function test_initial_const_method() @@ -86,6 +86,6 @@ public function test_exception_when_class_has_no_states() use EnumerableStates; }; - $class->state('foo'); + $class->assign('foo'); } } diff --git a/tests/EnumerableTest.php b/tests/EnumerableTest.php index 5ce3569..ba1f382 100644 --- a/tests/EnumerableTest.php +++ b/tests/EnumerableTest.php @@ -101,7 +101,7 @@ public function test_returns_current_state() $states = ['foo', 'bar', 'quz']; $enum = new Enumerable($states); - $enum->set('foo'); + $enum->assign('foo'); $this->assertTrue($enum->is('foo')); $this->assertFalse($enum->isNot('foo')); @@ -132,7 +132,7 @@ public function test_exception_when_sets_invalid_state() $enum = new Enumerable(['foo', 'bar', 'quz']); - $enum->set('qux'); + $enum->assign('qux'); } public function test_sets_state_programmatically_when_true() @@ -166,7 +166,7 @@ public function test_sets_state_programmatically_when_false() { $enum = new Enumerable(['foo', 'bar', 'quz']); - $enum->set('foo'); + $enum->assign('foo'); $enum->unless(true, 'bar'); diff --git a/tests/Models/AutoFillTest.php b/tests/Models/AutoFillTest.php index 00c710b..abea92b 100644 --- a/tests/Models/AutoFillTest.php +++ b/tests/Models/AutoFillTest.php @@ -5,7 +5,7 @@ use BadMethodCallException; use Orchestra\Testbench\TestCase; use Illuminate\Database\Eloquent\Model; -use DarkGhostHunter\Laratraits\Models\AutoFill; +use DarkGhostHunter\Laratraits\Eloquent\AutoFill; class AutoFillTest extends TestCase { diff --git a/tests/Models/DefaultColumnsTest.php b/tests/Models/DefaultColumnsTest.php index 3bd10cb..b0ac3fa 100644 --- a/tests/Models/DefaultColumnsTest.php +++ b/tests/Models/DefaultColumnsTest.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; -use DarkGhostHunter\Laratraits\Models\DefaultColumns; +use DarkGhostHunter\Laratraits\Eloquent\DefaultColumns; class DefaultColumnsTest extends TestCase { diff --git a/tests/Models/DynamicallyMutatesTest.php b/tests/Models/DynamicallyMutatesTest.php index e121fd6..07bb7b5 100644 --- a/tests/Models/DynamicallyMutatesTest.php +++ b/tests/Models/DynamicallyMutatesTest.php @@ -6,7 +6,7 @@ use Orchestra\Testbench\TestCase; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; -use DarkGhostHunter\Laratraits\Models\DynamicallyMutates; +use DarkGhostHunter\Laratraits\Eloquent\DynamicallyMutates; class DynamicallyMutatesTest extends TestCase { diff --git a/tests/Models/HasFileTest.php b/tests/Models/HasFileTest.php index 3519b00..fa90160 100644 --- a/tests/Models/HasFileTest.php +++ b/tests/Models/HasFileTest.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Storage; use Illuminate\Database\Schema\Blueprint; -use DarkGhostHunter\Laratraits\Models\HasFile; +use DarkGhostHunter\Laratraits\Eloquent\HasFile; class HasFileTest extends TestCase { diff --git a/tests/Models/HasSlugTest.php b/tests/Models/HasSlugTest.php index 522c894..a85f01c 100644 --- a/tests/Models/HasSlugTest.php +++ b/tests/Models/HasSlugTest.php @@ -9,7 +9,7 @@ use Illuminate\Foundation\Application; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; -use DarkGhostHunter\Laratraits\Models\HasSlug; +use DarkGhostHunter\Laratraits\Eloquent\HasSlug; class HasSlugTest extends TestCase { @@ -124,7 +124,7 @@ class TestSlugableBarModel extends Model protected $fillable = ['quz']; - public function attributeToSlug() + public function sluggableAttribute() { return 'quz'; } diff --git a/tests/Models/ModelTypeTest.php b/tests/Models/ModelTypeTest.php index 20094d2..d94c416 100644 --- a/tests/Models/ModelTypeTest.php +++ b/tests/Models/ModelTypeTest.php @@ -6,7 +6,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; -use DarkGhostHunter\Laratraits\Models\ModelType; +use DarkGhostHunter\Laratraits\Eloquent\ModelType; class ModelTypeTest extends TestCase { @@ -75,12 +75,12 @@ class CustomTypeTestModel extends BaseTestModel protected $table = 'bar'; - protected function getQualifiedTypeColumn() + public function getModelTypeColumn() { return 'bar'; } - protected function getTypeName() + public function getModelType() { return 'quz'; } diff --git a/tests/Models/NeighbourRecordsTest.php b/tests/Models/NeighbourRecordsTest.php index 1b811ec..309e20e 100644 --- a/tests/Models/NeighbourRecordsTest.php +++ b/tests/Models/NeighbourRecordsTest.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; -use DarkGhostHunter\Laratraits\Models\NeighbourRecords; +use DarkGhostHunter\Laratraits\Eloquent\NeighbourRecords; class NeighbourRecordsTest extends TestCase { diff --git a/tests/Models/SoftCachesAccessorsTest.php b/tests/Models/SoftCachesAccessorsTest.php index 4cc0de7..63ee6ce 100644 --- a/tests/Models/SoftCachesAccessorsTest.php +++ b/tests/Models/SoftCachesAccessorsTest.php @@ -5,7 +5,7 @@ use Orchestra\Testbench\TestCase; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; -use DarkGhostHunter\Laratraits\Models\SoftCachesAccessors; +use DarkGhostHunter\Laratraits\Eloquent\SoftCachesAccessors; class SoftCachesAccessorsTest extends TestCase { @@ -56,7 +56,7 @@ protected function getNotAttribute($value) ); $this->assertEquals(3, $model::$hits); - $model->withoutAccessorCache(function ($model) { + $model->withoutMutatorCache(function ($model) { $model->json; }); $this->assertEquals(4, $model::$hits); diff --git a/tests/Models/UsesUuidTest.php b/tests/Models/UsesUuidTest.php index 1d168e4..4a6abc4 100644 --- a/tests/Models/UsesUuidTest.php +++ b/tests/Models/UsesUuidTest.php @@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Eloquent\Collection; -use DarkGhostHunter\Laratraits\Models\UsesUuid; +use DarkGhostHunter\Laratraits\Eloquent\UsesUuid; use Illuminate\Database\Eloquent\ModelNotFoundException; class UsesUuidTest extends TestCase From 98b878fa01ce3f863e18ecb50baf36c90d9e6789 Mon Sep 17 00:00:00 2001 From: DarkGhostHunter Date: Sun, 24 May 2020 14:30:53 -0400 Subject: [PATCH 2/6] Added Upgrade guide. --- UPGRADE.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 UPGRADE.md diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..e3fcc36 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,93 @@ +Upgrading from v1.x to v2.x + +## Removed + +The following traits and classes has been removed: + +* `ClassDiscoverer`, +* `DiscoverClasses`, +* `DispatchesItself`, +* `HasFile`. +* `ThrottlesRequest`, + +For `HasFile`, migrate to [Spatie's Laravel Medialibrary](https://github.com/spatie/laravel-medialibrary) package instead. + +For `ThrottlesRequest`, migrate to [Laravel Rate Limiting](https://github.com/laravel/framework/pull/32726). + +## Changed + +### Namespace change + +* The `Laratraits\Models` namespace has been changed to `Laratraits\Eloquent`. + +## AutoFill trait changed + +* The trait will now call Mutators when filling automatically a property. + +## DefaultColumns trait changed + +* It now incorporates the `withoutDefaultColumns()` method to disable the global scope that is applied automatically. + +## DynamicallyMutates removed + +* The `DynamicallyMutates` trait has been removed. Use [Custom Casts](https://laravel.com/docs/eloquent-mutators#custom-casts) instead. + +### Enumerable changed + +* The `Enumerable` class incorporates a new method called `assign` that assigns a given state, alternatively to calling dynamically a method call. + +### HasSlug changed + +* The `attributeToSlug` method has changed to `sluggableAttribute` and now returns the string `title`. + +* The `setSlugAttribute` method has changed to `slugValue`, and returns the slug value. + +* The `initializeHasSlug` method has been removed. + +* The `setSlug` method has been added. It automatically sets the slug attribute with the slug value from a given attribute value. + +* The slug will be created on saving the model only when the sluggable attribute has been modified. + +* Routing now is optional. To disable routing by the slug, declare the `$routeBySlug` property as `false`. + +## MacrosEloquent changed + +* Now the trait will automatically re-route all Eloquent builder macros to the public static methods in the Scope. There is no longer need to return closures or what not. + +> This can have up to 100% more performance and makes the code more readable. + +### ModelType changed + +* The `getQualifiedTypeColumn` method has been changed to `getModelTypeColumn`. + +* The `getTypeName` method has been changed to `getModelType`, and now returns the class name as snake case by default. + +### NeighbourRecords changed + +* The trait now gets the latest and oldest records from the given model, using the creation timestamp of these. + +* It now includes the `filterNeighbourQuery` that allows to further filter the records to retrieve. + +* It also includes the `queryColumns` to filter which columns to retrieve. It defaults to only the key used for routing (which is the primary key by default) + +## ValidateConsumableSignature changed + +* The middleware no longer uses `terminate`, but rather, evaluates the response before sending it to the browser. This shrinks the window where a user can make two times the same request. + +## SoftCachesAccessors changed + +* The `$cachedAccessorsValues` is now `$cachedMutators`. + +* The `withoutAccessorCache()` is now `withoutMutatorCache()`. + +* The `cachedAccessors()` is now `cachedMutators()`. + +## UsesUuid changed + +* The `getQualifiedUuidColumn()` is now `getUuidColumn()`. + +* The `shouldAddGlobalScope` is now `addUuidGlobalScope()`. + +## UuidScope changed + +* Now all the scopes has been changed to public static methods to follow the changes on the `MacrosEloquent` trait. From 9d9c10fffe08936d4fee1a42b976cdbfae7d8cee Mon Sep 17 00:00:00 2001 From: DarkGhostHunter Date: Sun, 24 May 2020 17:32:50 -0400 Subject: [PATCH 3/6] Added Throttler for class methods. --- README.md | 9 +- src/ActionRateLimiter.php | 181 ++++++++++++++++++++++++++++++++++++++ src/ThrottleActions.php | 67 ++++++++++++++ 3 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 src/ActionRateLimiter.php create mode 100644 src/ThrottleActions.php diff --git a/README.md b/README.md index 034f84e..a9fe3dc 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Before installing, take a look into the list. If you're only using one, just cop Just remember to **change the namespace** if you're copy-pasting them! -### Traits for everyone +### Traits for everything * [`EnumerableStates`](src/EnumerableStates.php): Allows a class instance to have a single allowed state. * [`Multitaps`](src/Multitaps.php): Makes all class methods _chainable_, like using `tap()` but forever. You can exit the tap using `->target` or a method name appended with `AndUntap`. @@ -63,6 +63,7 @@ Just remember to **change the namespace** if you're copy-pasting them! * [`SavesToSession`](src/SavesToSession.php): Saves the object (or part of it) to the session. * [`SavesToStorage`](src/SavesToStorage.php): Saves the object (or part of it) to the storage. * [`SendsToHttp`](src/SendsToHttp.php): Sends the object (or part of it) through an HTTP Request. +* [`ThrottleActions`](src/ThrottleActions.php): Throttles a given method in a class easily. * [`ValidatesItself`](src/ValidatesItself.php): Validates an incoming data using self-contained rules. ### Useful classes @@ -72,11 +73,11 @@ Just remember to **change the namespace** if you're copy-pasting them! ### Models * [`AutoFill`](src/Eloquent/AutoFill.php): Automatically fills the Model with values by each method name, like `fillFooAttribute()`. -* [`DefaultColumns`](src/Eloquent/DefaultColumns.php): Adds a `DefaultColumns` Global Scope to the Model that selects only given default columns, unless overrun manually in the query. +* [`DefaultColumns`](src/Eloquent/DefaultColumns.php): Adds a `DefaultColumns` Global Scope to the Model selecting only given default columns, unless overrun manually in the query. * [`HasSlug`](src/Eloquent/HasSlug.php): Allows a Model to be bound to routes using the slug like `this-is-the-model`. Requires a new column in the table. -* [`ModelType`](src/Eloquent/ModelType.php): Useful for Models that share a single table but have different "types". +* [`ModelType`](src/Eloquent/ModelType.php): Useful for Models that share a single table but have different "types", like Publications: Article, Post, Note, etc. * [`NeighbourRecords`](src/Eloquent/NeighbourRecords.php): Allows to easily get the "next" and "previous" record from a given model. -* [`SoftCachesAccessors`](src/Eloquent/SoftCachesAccessors.php): Saves the result of a accessor to avoid running the accessor logic again. Overrides the `mutateAttribute()` method. +* [`SoftCachesAccessors`](src/Eloquent/SoftCachesAccessors.php): Saves the result of an accessor to avoid running the accessor logic again. Overrides the `mutateAttribute()` method. * [`UsesUuid`](src/Eloquent/UsesUuid.php): Automatically fills the UUID on the Model. Comes with an optional Eloquent Query Builder local scopes. You can override the UUID generation. #### Casts diff --git a/src/ActionRateLimiter.php b/src/ActionRateLimiter.php new file mode 100644 index 0000000..3d885ba --- /dev/null +++ b/src/ActionRateLimiter.php @@ -0,0 +1,181 @@ +limiter = $limiter; + } + + /** + * Sets the limits to use with the Rate Limiter. + * + * @param object $target + * @param int $tries + * @param int $minutes + * @param callable|null $default + * @return \DarkGhostHunter\Laratraits\ActionRateLimiter + */ + public function throttle(object $target, int $tries, int $minutes, callable $default = null) + { + $this->target = $target; + $this->maxAttempts = $tries; + $this->decaySeconds = $minutes * 60; + $this->default = $default; + + return $this; + } + + /** + * Checks if there are too many attempts for the method. + * + * @param string $method + * @return bool + */ + public function throttlerTooManyAttempts(string $method) + { + return $this->limiter->tooManyAttempts($this->actionRateLimiterKey($method), $this->maxAttempts); + } + + /** + * Clear the throttler. + * + * @param string $method + * @return void + */ + public function throttlerClear(string $method) + { + $this->limiter->clear($this->actionRateLimiterKey($method)); + } + + /** + * Resets the number of tries to the method. + * + * @param string $method + * @return mixed + */ + public function throttlerReset(string $method) + { + return $this->limiter->resetAttempts($this->actionRateLimiterKey($method)); + } + + /** + * Hits the throttler for the given method. + * + * @param string $method + * @return mixed + */ + public function throttlerHit(string $method) + { + return $this->limiter->hit($this->actionRateLimiterKey($method), $this->decaySeconds); + } + + /** + * Returns the Action Rate Limiter key. + * + * @param string|null $name + * @return string + */ + protected function actionRateLimiterKey(string $name) + { + return 'class_method_throttler|' . get_class($this->target) . '@' . $name; + } + + /** + * Handle dynamically calling the object. + * + * @param string $name + * @param mixed $arguments + * @return object + */ + public function __call($name, $arguments) + { + if (! $this->throttlerTooManyAttempts($name)) { + $this->target->{$name}(...$arguments); + $this->throttlerHit($name); + } + elseif ($this->default) { + call_user_func($this->default, $this->target, $this); + } + + return $this->target; + } +} diff --git a/src/ThrottleActions.php b/src/ThrottleActions.php new file mode 100644 index 0000000..4eed295 --- /dev/null +++ b/src/ThrottleActions.php @@ -0,0 +1,67 @@ +throttle(60, 1)->heavilyComputational($parameters); + * + * Alternatively, you can pass a callable to the limit that will be executed if there are + * too many hits for the action. The callable receives the object as first as parameter + * and the ActionRateLimiter as second parameter, where you can use the Rate Limiter. + * + * $object->throttle(60, 1, function ($object, $limiter) { + * $limiter->throttlerClear('heavilyComputational'); + * $object->doSomethingElse(); + * })->heavilyComputational($parameters); + * + * If you need granular control on the cache key, use the fourth parameter with name: + * + * $object->throttler(60, 1, null, 'my_custom_key')->heavilyComputational($parameters); + * + * --- + * MIT License + * + * Copyright (c) Italo Israel Baeza Cabrera + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2020 Laravel LLC. + * + * @link https://github.com/DarkGhostHunter/Laratraits + */ + +namespace DarkGhostHunter\Laratraits; + +trait ThrottleActions +{ + /** + * Limits the next method call by a given window of time. + * + * @param int $tries + * @param int $minutes + * @param callable|null $default + * @return \DarkGhostHunter\Laratraits\ActionRateLimiter + */ + public function throttle($tries = 60, $minutes = 1, $default = null) + { + return app(ActionRateLimiter::class)->throttle($this, $tries, $minutes, $default); + } +} From 00949462a120fefb3f8b19cdc4805a8cf9f17d00 Mon Sep 17 00:00:00 2001 From: DarkGhostHunter Date: Mon, 25 May 2020 01:55:33 -0400 Subject: [PATCH 4/6] Completed with tests. --- README.md | 8 +- UPGRADE.md | 13 +- composer.json | 6 +- ...teLimiter.php => ClassMethodThrottler.php} | 34 +-- src/Eloquent/Casts/CastEnumerable.php | 6 +- ...CastsRepository.php => CastRepository.php} | 27 +- src/Eloquent/DefaultColumns.php | 2 +- .../{AutoFill.php => FillsAttributes.php} | 14 +- src/Eloquent/HasSlug.php | 4 +- src/Eloquent/NeighbourRecords.php | 19 +- src/Eloquent/SoftCachesAccessors.php | 130 --------- src/Enumerable.php | 8 +- src/FiresItself.php | 4 +- src/Scopes/DefaultColumns.php | 2 +- src/Scopes/MacrosEloquent.php | 5 +- src/SendsToHttp.php | 10 +- .../RegisterBladeExtensions.php | 13 +- ...ersObservers.php => RegisterObservers.php} | 2 +- ...hrottleActions.php => ThrottleMethods.php} | 31 ++- tests/Controllers/ThrottlesRequestTest.php | 62 ----- tests/Eloquent/Casts/CastEnumerableTest.php | 84 ++++++ tests/Eloquent/Casts/CastRepositoryTest.php | 86 ++++++ .../DefaultColumnsTest.php | 24 +- .../FillsAttributesTest.php} | 26 +- tests/{Models => Eloquent}/HasSlugTest.php | 57 ++-- tests/{Models => Eloquent}/ModelTypeTest.php | 8 +- tests/Eloquent/NeighbourRecordsTest.php | 125 +++++++++ tests/{Models => Eloquent}/UsesUuidTest.php | 6 +- tests/EnumerableStatesTest.php | 21 +- tests/EnumerableTest.php | 18 +- tests/FiresItselfTest.php | 54 ++++ tests/Middleware/CacheStaticResponseTest.php | 4 +- .../Middleware/ShareAuthenticatedUserTest.php | 2 +- .../ValidateConsumableSignatureTest.php | 2 +- tests/Models/DynamicallyMutatesTest.php | 123 --------- tests/Models/HasFileTest.php | 254 ------------------ tests/Models/NeighbourRecordsTest.php | 65 ----- tests/Models/SoftCachesAccessorsTest.php | 64 ----- tests/MultitapsTest.php | 6 +- tests/PipesThroughTest.php | 14 +- tests/RendersFromMarkdownTest.php | 12 +- tests/SavesToCacheTest.php | 66 +---- tests/SavesToSessionTest.php | 66 +---- tests/SavesToStorageTest.php | 73 +---- tests/SendsToHttpTest.php | 51 ++++ .../RegisterBladeExtensionsTest.php | 59 ++++ tests/ServiceProviders/RegisterGatesTest.php | 36 +++ .../RegisterObserversTest.php | 74 +++++ .../TestDiscoverableClassQuz.php | 18 -- tests/Stubs/TestDiscoverableClassBar.php | 9 - tests/Stubs/TestDiscoverableClassFoo.php | 9 - tests/Stubs/TestInterface.php | 7 - tests/Stubs/TestNotDiscoverable.php | 6 - tests/Stubs/TextFileTest.txt | 9 - tests/ThrottlesMethodsTest.php | 82 ++++++ tests/ValidatesItselfTest.php | 18 +- 56 files changed, 898 insertions(+), 1140 deletions(-) rename src/{ActionRateLimiter.php => ClassMethodThrottler.php} (88%) rename src/Eloquent/Casts/{CastsRepository.php => CastRepository.php} (77%) rename src/Eloquent/{AutoFill.php => FillsAttributes.php} (91%) delete mode 100644 src/Eloquent/SoftCachesAccessors.php rename src/ServiceProviders/{RegistersObservers.php => RegisterObservers.php} (98%) rename src/{ThrottleActions.php => ThrottleMethods.php} (77%) delete mode 100644 tests/Controllers/ThrottlesRequestTest.php create mode 100644 tests/Eloquent/Casts/CastEnumerableTest.php create mode 100644 tests/Eloquent/Casts/CastRepositoryTest.php rename tests/{Models => Eloquent}/DefaultColumnsTest.php (78%) rename tests/{Models/AutoFillTest.php => Eloquent/FillsAttributesTest.php} (76%) rename tests/{Models => Eloquent}/HasSlugTest.php (56%) rename tests/{Models => Eloquent}/ModelTypeTest.php (90%) create mode 100644 tests/Eloquent/NeighbourRecordsTest.php rename tests/{Models => Eloquent}/UsesUuidTest.php (97%) create mode 100644 tests/FiresItselfTest.php delete mode 100644 tests/Models/DynamicallyMutatesTest.php delete mode 100644 tests/Models/HasFileTest.php delete mode 100644 tests/Models/NeighbourRecordsTest.php delete mode 100644 tests/Models/SoftCachesAccessorsTest.php create mode 100644 tests/SendsToHttpTest.php create mode 100644 tests/ServiceProviders/RegisterBladeExtensionsTest.php create mode 100644 tests/ServiceProviders/RegisterGatesTest.php create mode 100644 tests/ServiceProviders/RegisterObserversTest.php delete mode 100644 tests/Stubs/TestDirectory/TestDiscoverableClassQuz.php delete mode 100644 tests/Stubs/TestDiscoverableClassBar.php delete mode 100644 tests/Stubs/TestDiscoverableClassFoo.php delete mode 100644 tests/Stubs/TestInterface.php delete mode 100644 tests/Stubs/TestNotDiscoverable.php delete mode 100644 tests/Stubs/TextFileTest.txt create mode 100644 tests/ThrottlesMethodsTest.php diff --git a/README.md b/README.md index a9fe3dc..bd412ac 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Just remember to **change the namespace** if you're copy-pasting them! ### Traits for everything * [`EnumerableStates`](src/EnumerableStates.php): Allows a class instance to have a single allowed state. +* [`FiresItself`](src/FiresItself.php): Allows an Event to be fired conveniently. * [`Multitaps`](src/Multitaps.php): Makes all class methods _chainable_, like using `tap()` but forever. You can exit the tap using `->target` or a method name appended with `AndUntap`. * [`PipesThrough`](src/PipesThrough.php): Allows a class to be piped through a pipeline immediately or to a queue. * [`RendersFromMarkdown`](src/RendersFromMarkdown.php): Takes a given class property to parse Markdown text and return HTML. Compatible with `Htmlable` interface. @@ -63,7 +64,7 @@ Just remember to **change the namespace** if you're copy-pasting them! * [`SavesToSession`](src/SavesToSession.php): Saves the object (or part of it) to the session. * [`SavesToStorage`](src/SavesToStorage.php): Saves the object (or part of it) to the storage. * [`SendsToHttp`](src/SendsToHttp.php): Sends the object (or part of it) through an HTTP Request. -* [`ThrottleActions`](src/ThrottleActions.php): Throttles a given method in a class easily. +* [`ThrottleActions`](src/ThrottleMethods.php): Throttles a given method in a class easily. * [`ValidatesItself`](src/ValidatesItself.php): Validates an incoming data using self-contained rules. ### Useful classes @@ -72,18 +73,17 @@ Just remember to **change the namespace** if you're copy-pasting them! ### Models -* [`AutoFill`](src/Eloquent/AutoFill.php): Automatically fills the Model with values by each method name, like `fillFooAttribute()`. +* [`AutoFill`](src/Eloquent/FillsAttributes.php): Automatically fills the Model with values by each method name, like `fillFooAttribute()`. * [`DefaultColumns`](src/Eloquent/DefaultColumns.php): Adds a `DefaultColumns` Global Scope to the Model selecting only given default columns, unless overrun manually in the query. * [`HasSlug`](src/Eloquent/HasSlug.php): Allows a Model to be bound to routes using the slug like `this-is-the-model`. Requires a new column in the table. * [`ModelType`](src/Eloquent/ModelType.php): Useful for Models that share a single table but have different "types", like Publications: Article, Post, Note, etc. * [`NeighbourRecords`](src/Eloquent/NeighbourRecords.php): Allows to easily get the "next" and "previous" record from a given model. -* [`SoftCachesAccessors`](src/Eloquent/SoftCachesAccessors.php): Saves the result of an accessor to avoid running the accessor logic again. Overrides the `mutateAttribute()` method. * [`UsesUuid`](src/Eloquent/UsesUuid.php): Automatically fills the UUID on the Model. Comes with an optional Eloquent Query Builder local scopes. You can override the UUID generation. #### Casts * [`CastEnumerable`](src/Eloquent/Casts/CastEnumerable.php): Allows a custom Enumerable class to be [_castable_](https://laravel.com/docs/eloquent-mutators#custom-casts) in a model. -* [`CastsRepository`](src/Eloquent/Casts/CastsRepository.php): Allows an array property to be cased as a Repository. +* [`CastsRepository`](src/Eloquent/Casts/CastRepository.php): Allows an array property to be cased as a Repository. ### Global Scopes diff --git a/UPGRADE.md b/UPGRADE.md index e3fcc36..d540ceb 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,11 +8,12 @@ The following traits and classes has been removed: * `DiscoverClasses`, * `DispatchesItself`, * `HasFile`. +* `SoftCachesAccessors`, * `ThrottlesRequest`, For `HasFile`, migrate to [Spatie's Laravel Medialibrary](https://github.com/spatie/laravel-medialibrary) package instead. -For `ThrottlesRequest`, migrate to [Laravel Rate Limiting](https://github.com/laravel/framework/pull/32726). +For `ThrottlesRequest`, migrate to [Laravel Rate Limiting](https://github.com/laravel/framework/pull/32726) once it becomes available. ## Changed @@ -22,6 +23,8 @@ For `ThrottlesRequest`, migrate to [Laravel Rate Limiting](https://github.com/la ## AutoFill trait changed +* The trait has been renamed to `FillsAttributes` + * The trait will now call Mutators when filling automatically a property. ## DefaultColumns trait changed @@ -74,14 +77,6 @@ For `ThrottlesRequest`, migrate to [Laravel Rate Limiting](https://github.com/la * The middleware no longer uses `terminate`, but rather, evaluates the response before sending it to the browser. This shrinks the window where a user can make two times the same request. -## SoftCachesAccessors changed - -* The `$cachedAccessorsValues` is now `$cachedMutators`. - -* The `withoutAccessorCache()` is now `withoutMutatorCache()`. - -* The `cachedAccessors()` is now `cachedMutators()`. - ## UsesUuid changed * The `getQualifiedUuidColumn()` is now `getUuidColumn()`. diff --git a/composer.json b/composer.json index 9d2f725..f99a798 100644 --- a/composer.json +++ b/composer.json @@ -22,11 +22,11 @@ "require": { "php": "^7.2.15", "ext-json": "*", - "illuminate/support": "7.*", - "symfony/finder": "5.*" + "illuminate/support": "^7.0", + "guzzlehttp/guzzle": "^6.5" }, "require-dev": { - "orchestra/testbench": "5.*" + "orchestra/testbench": "^5.2", }, "autoload": { "psr-4": { diff --git a/src/ActionRateLimiter.php b/src/ClassMethodThrottler.php similarity index 88% rename from src/ActionRateLimiter.php rename to src/ClassMethodThrottler.php index 3d885ba..043f968 100644 --- a/src/ActionRateLimiter.php +++ b/src/ClassMethodThrottler.php @@ -1,6 +1,6 @@ target = $target; $this->maxAttempts = $tries; $this->decaySeconds = $minutes * 60; $this->default = $default; @@ -105,14 +103,16 @@ public function throttle(object $target, int $tries, int $minutes, callable $def } /** - * Checks if there are too many attempts for the method. + * Sets the target object to throttle its methods. * - * @param string $method - * @return bool + * @param object $target + * @return $this */ - public function throttlerTooManyAttempts(string $method) + public function setTarget(object $target) { - return $this->limiter->tooManyAttempts($this->actionRateLimiterKey($method), $this->maxAttempts); + $this->target = $target; + + return $this; } /** @@ -127,14 +127,14 @@ public function throttlerClear(string $method) } /** - * Resets the number of tries to the method. + * Checks if there are too many attempts for the method. * * @param string $method - * @return mixed + * @return bool */ - public function throttlerReset(string $method) + public function throttlerTooManyAttempts(string $method) { - return $this->limiter->resetAttempts($this->actionRateLimiterKey($method)); + return $this->limiter->tooManyAttempts($this->actionRateLimiterKey($method), $this->maxAttempts); } /** @@ -172,8 +172,8 @@ public function __call($name, $arguments) $this->target->{$name}(...$arguments); $this->throttlerHit($name); } - elseif ($this->default) { - call_user_func($this->default, $this->target, $this); + else { + with($this->target, $this->default); } return $this->target; diff --git a/src/Eloquent/Casts/CastEnumerable.php b/src/Eloquent/Casts/CastEnumerable.php index 9a87a05..ebf5777 100644 --- a/src/Eloquent/Casts/CastEnumerable.php +++ b/src/Eloquent/Casts/CastEnumerable.php @@ -61,7 +61,7 @@ abstract class CastEnumerable extends Enumerable implements CastsAttributes */ public function get($model, string $key, $value, array $attributes) { - return static::as($value); + return $this->when($value, $value); } /** @@ -69,6 +69,8 @@ public function get($model, string $key, $value, array $attributes) */ public function set($model, string $key, $value, array $attributes) { - return (is_string($value) ? static::as($value) : $value)->current(); + $this->when($value, $value); + + return (string)$this->current(); } } diff --git a/src/Eloquent/Casts/CastsRepository.php b/src/Eloquent/Casts/CastRepository.php similarity index 77% rename from src/Eloquent/Casts/CastsRepository.php rename to src/Eloquent/Casts/CastRepository.php index ced3ea5..09c30fe 100644 --- a/src/Eloquent/Casts/CastsRepository.php +++ b/src/Eloquent/Casts/CastRepository.php @@ -46,14 +46,14 @@ use Illuminate\Config\Repository; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; -class CastsRepository implements CastsAttributes +class CastRepository implements CastsAttributes { /** * @inheritDoc */ public function get($model, string $key, $value, array $attributes) { - return new Repository($value); + return new Repository(empty($value) ? [] : json_decode($value, true)); } /** @@ -61,10 +61,27 @@ public function get($model, string $key, $value, array $attributes) */ public function set($model, string $key, $value, array $attributes) { - if ($value instanceof Repository) { - return $value->all(); + if ($this->isJson($value)) { + return $value; } - return (array)$value; + return json_encode($value instanceof Repository ? $value->all() : (array)$value); + } + + /** + * Check if the value is JSON. + * + * @param $string + * @return bool + */ + protected function isJson($string) + { + if (is_string($string)) { + json_decode($string); + + return (json_last_error() === JSON_ERROR_NONE); + } + + return false; } } diff --git a/src/Eloquent/DefaultColumns.php b/src/Eloquent/DefaultColumns.php index c07af0a..da4fe4f 100644 --- a/src/Eloquent/DefaultColumns.php +++ b/src/Eloquent/DefaultColumns.php @@ -70,7 +70,7 @@ protected static function bootDefaultColumns() * * @return array */ - protected static function getDefaultColumns() + protected static function getDefaultColumns() : array { return static::$defaultColumns ?? []; } diff --git a/src/Eloquent/AutoFill.php b/src/Eloquent/FillsAttributes.php similarity index 91% rename from src/Eloquent/AutoFill.php rename to src/Eloquent/FillsAttributes.php index 57bbef7..4c68c0f 100644 --- a/src/Eloquent/AutoFill.php +++ b/src/Eloquent/FillsAttributes.php @@ -1,6 +1,6 @@ autoFillable() as $attribute) { @@ -62,14 +62,16 @@ protected function initializeAutoFill() } try { - $result = $this->{'fill' . Str::studly($attribute) . 'Attribute'}($attribute); + $result = $this->{'fill' . Str::studly($attribute) . 'Attribute'}(); + + if ($result && ! isset($this->attributes[$attribute])) { + $this->setAttribute($attribute, $result); + } } catch (BadMethodCallException $exception) { throw new BadMethodCallException( "The attribute [$attribute] has no a filler method [fill".Str::studly($attribute)."Attribute]." ); } - - $this->setAttribute($attribute, $result); } } diff --git a/src/Eloquent/HasSlug.php b/src/Eloquent/HasSlug.php index 06efd0d..3f229ac 100644 --- a/src/Eloquent/HasSlug.php +++ b/src/Eloquent/HasSlug.php @@ -57,7 +57,9 @@ trait HasSlug protected static function bootHasSlug() { static::saving(function ($model) { - if ($model->isDirty($model->attributeToSlug())) { + $sluggable = $model->sluggableAttribute(); + + if (! $model->getAttribute($sluggable) || $model->isDirty($sluggable)) { $model->setSlug(); } }); diff --git a/src/Eloquent/NeighbourRecords.php b/src/Eloquent/NeighbourRecords.php index ebe8717..2c6d70c 100644 --- a/src/Eloquent/NeighbourRecords.php +++ b/src/Eloquent/NeighbourRecords.php @@ -74,27 +74,28 @@ protected function getNeighbourRecords() * Returns the record list. * * @return array - * @throws \Exception */ protected function getRecordsList() { return cache() ->remember("query|{$this->getQualifiedKeyName()}_{$this->getKey()}|neighbours", 60, function () { return [ - 'next' => $this->queryNextRecord(), 'prev' => $this->queryPrevRecord(), + 'next' => $this->queryNextRecord(), ]; }); } /** - * Retrieves the next model. + * Retrieves the previous model. * * @return null|static */ - protected function queryNextRecord() + protected function queryPrevRecord() { - $builder = $this->latest()->where($this->getCreatedAtColumn(), '>', $this->{$this->getCreatedAtColumn()}); + $builder = $this->latest() + ->whereKeyNot($this->getKey()) + ->where($this->getCreatedAtColumn(), '<=', $this->{$this->getCreatedAtColumn()}); $this->filterNeighbourQuery($builder); @@ -102,13 +103,15 @@ protected function queryNextRecord() } /** - * Retrieves the previous model. + * Retrieves the next model. * * @return null|static */ - protected function queryPrevRecord() + protected function queryNextRecord() { - $builder = $this->oldest()->where($this->getCreatedAtColumn(), '<', $this->{$this->getCreatedAtColumn()}); + $builder = $this->oldest() + ->whereKeyNot($this->getKey()) + ->where($this->getCreatedAtColumn(), '>=', $this->{$this->getCreatedAtColumn()}); $this->filterNeighbourQuery($builder); diff --git a/src/Eloquent/SoftCachesAccessors.php b/src/Eloquent/SoftCachesAccessors.php deleted file mode 100644 index f4b52c1..0000000 --- a/src/Eloquent/SoftCachesAccessors.php +++ /dev/null @@ -1,130 +0,0 @@ -random_number; // 63 - * echo $model->random_number; // 63 - * - * --- - * MIT License - * - * Copyright (c) Italo Israel Baeza Cabrera - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2020 Laravel LLC. - * - * @link https://github.com/DarkGhostHunter/Laratraits - */ - -namespace DarkGhostHunter\Laratraits\Eloquent; - -use Closure; - -trait SoftCachesAccessors -{ - /** - * List of cached accessor values - * - * @var array - */ - protected $cachedMutators = []; - - /** - * Returns the value of an attribute using its cached accessor. - * - * @see \Illuminate\Database\Eloquent\Concerns\HasAttributes - * @param string $key - * @param mixed $value - * @return mixed - */ - protected function mutateAttribute($key, $value) - { - if (! in_array($key, $this->cachedMutators(), true)) { - return parent::mutateAttribute($key, $value); - } - - return $this->cachedMutators[$key] = $this->cachedMutators[$key] - ?? parent::mutateAttribute($key, $value); - } - - /** - * Flush all the cached accessors - * - * @return $this - */ - public function flushAccessorsCache() - { - $this->cachedMutators = []; - - return $this; - } - - /** - * Returns an attribute without using the cache. - * - * @param $key - * @return mixed - */ - public function getAttributeWithoutCache($key) - { - return parent::mutateAttribute($key, $this->getAttributeFromArray($key)); - } - - /** - * Runs a callback without using the accessor cache - * - * @param \Closure $callback - * @return mixed - */ - public function withoutMutatorCache(Closure $callback) - { - $accessors = $this->cachedMutators; - - $this->flushAccessorsCache(); - - $value = $callback($this); - - $this->cachedMutators = $accessors; - - return $value; - } - - /** - * The accessors to cache in the model - * - * @return array - */ - protected function cachedMutators() - { - return $this->cachedMutators; - } -} diff --git a/src/Enumerable.php b/src/Enumerable.php index 18f064a..268afe9 100644 --- a/src/Enumerable.php +++ b/src/Enumerable.php @@ -154,10 +154,10 @@ public function assign(string $name) * Sets an state when a given condition evaluates to true. * * @param bool|\Closure $condition - * @param string $state + * @param string|null $state * @return $this */ - public function when($condition, string $state) + public function when($condition, $state) { if (value($condition)) { return $this->assign($state); @@ -265,8 +265,8 @@ public static function from($states, string $initial = null) : self * @param string|null $initial * @return mixed */ - public static function as(string $initial) : self + public static function as(string $initial = null) : self { - return (new static)->assign($initial); + return (new static)->when($initial, $initial); } } diff --git a/src/FiresItself.php b/src/FiresItself.php index 761b57c..2dfe3ff 100644 --- a/src/FiresItself.php +++ b/src/FiresItself.php @@ -49,7 +49,7 @@ trait FiresItself */ public static function fire(...$args) { - return app('events')->dispatch(static::class, new static(...$args)); + return app('events')->dispatch(new static(...$args)); } /** @@ -60,6 +60,6 @@ public static function fire(...$args) */ public static function fireHalted(...$args) { - return app('events')->dispatch(static::class, new static(...$args), true); + return app('events')->dispatch(new static(...$args), [], true); } } diff --git a/src/Scopes/DefaultColumns.php b/src/Scopes/DefaultColumns.php index 560fbb1..3aa8369 100644 --- a/src/Scopes/DefaultColumns.php +++ b/src/Scopes/DefaultColumns.php @@ -63,7 +63,7 @@ public function __construct(array $defaultColumns) */ public function apply(Builder $builder, Model $model) { - if ($this->defaultColumns === [] && empty($builder->getQuery()->columns)) { + if ($this->defaultColumns !== [] && empty($builder->getQuery()->columns)) { return $builder->select($this->defaultColumns); } diff --git a/src/Scopes/MacrosEloquent.php b/src/Scopes/MacrosEloquent.php index 10cf9ea..d73bfb5 100644 --- a/src/Scopes/MacrosEloquent.php +++ b/src/Scopes/MacrosEloquent.php @@ -57,8 +57,9 @@ public function extend(Builder $builder) ->getMethods(ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_STATIC); foreach ($methods as $method) { - if (strpos($method->getName(), 'macro') === 0) { - $builder->macro(lcfirst(substr($method, 5)), static::class . '::' . $method); + + if (strpos($name = $method->getName(), 'macro') === 0) { + $builder->macro(lcfirst(substr($name, 5)), [static::class, $name]); } } } diff --git a/src/SendsToHttp.php b/src/SendsToHttp.php index e063202..dc0a2ea 100644 --- a/src/SendsToHttp.php +++ b/src/SendsToHttp.php @@ -37,7 +37,7 @@ namespace DarkGhostHunter\Laratraits; use LogicException; -use Illuminate\Http\Client\Factory; +use Illuminate\Support\Facades\Http; use Illuminate\Http\Client\PendingRequest; trait SendsToHttp @@ -51,7 +51,7 @@ trait SendsToHttp public function send(string $url = null) { return $this->performHttpRequest( - $this->createPendingRequest(), $url ?? $this->url() + $this->httpRequestFactory(), $url ?? $this->url() ); } @@ -65,7 +65,7 @@ public function send(string $url = null) */ protected function performHttpRequest(PendingRequest $request, string $url) { - return $request->asJson()->post($url, $this->toHttp()); + return $request->post($url, $this->toHttp()); } /** @@ -73,9 +73,9 @@ protected function performHttpRequest(PendingRequest $request, string $url) * * @return \Illuminate\Http\Client\PendingRequest */ - protected function createPendingRequest() + protected function httpRequestFactory() : PendingRequest { - return new PendingRequest(new Factory); + return Http::getFacadeRoot()->asJson(); } /** diff --git a/src/ServiceProviders/RegisterBladeExtensions.php b/src/ServiceProviders/RegisterBladeExtensions.php index 7cda19c..6783ecb 100644 --- a/src/ServiceProviders/RegisterBladeExtensions.php +++ b/src/ServiceProviders/RegisterBladeExtensions.php @@ -3,9 +3,11 @@ * RegisterBladeExtensions * * This traits allows you to register multiple Blade extensions in just a few arrays. + * The Blade compiler uses **callables** for directives and the custom if statements + * so you will need to use public static methods and them as class@method notation. * * protected $directives = [ - * 'package-alert' => 'App\Blade\Directives\AlertComponent', + * 'package-alert' => 'App\Blade\Directives\AlertComponent@alert', * 'status-now' => 'App\Blade\Directives\Status@now', * ] * @@ -58,6 +60,7 @@ namespace DarkGhostHunter\Laratraits\ServiceProviders; +use Illuminate\Support\Str; use Illuminate\Support\Facades\Blade; trait RegisterBladeExtensions @@ -73,15 +76,15 @@ protected function registerBladeExtensions() $compiler = Blade::getFacadeRoot(); foreach ($this->directives ?? [] as $name => $handler) { - $compiler->directive($name, $handler); + $compiler->directive($name, Str::parseCallback($handler)); } foreach ($this->if ?? [] as $name => $handler) { - $compiler->if($name, $handler); + $compiler->if($name, Str::parseCallback($handler)); } - foreach ($this->include ?? [] as $name => $handler) { - $compiler->include($name, $handler); + foreach ($this->include ?? [] as $name => $view) { + $compiler->include($name, $view); } } } diff --git a/src/ServiceProviders/RegistersObservers.php b/src/ServiceProviders/RegisterObservers.php similarity index 98% rename from src/ServiceProviders/RegistersObservers.php rename to src/ServiceProviders/RegisterObservers.php index 97704fe..e1e9759 100644 --- a/src/ServiceProviders/RegistersObservers.php +++ b/src/ServiceProviders/RegisterObservers.php @@ -47,7 +47,7 @@ namespace DarkGhostHunter\Laratraits\ServiceProviders; -trait RegistersObservers +trait RegisterObservers { /** * Registers an array of handlers for a Eloquent event. diff --git a/src/ThrottleActions.php b/src/ThrottleMethods.php similarity index 77% rename from src/ThrottleActions.php rename to src/ThrottleMethods.php index 4eed295..159769c 100644 --- a/src/ThrottleActions.php +++ b/src/ThrottleMethods.php @@ -1,6 +1,6 @@ throttle($this, $tries, $minutes, $default); + return $this->getActionThrottler()->throttle($tries, $minutes, $default); + } + + /** + * Clear the throttler method. + * + * @param string $method + * @return \DarkGhostHunter\Laratraits\ThrottleMethods + */ + public function throttleClear(string $method) + { + $this->getActionThrottler()->throttlerClear($method); + + return $this; + } + + /** + * Return the Action Throttler instance. + * + * @return \DarkGhostHunter\Laratraits\ClassMethodThrottler + */ + protected function getActionThrottler() + { + return app(ClassMethodThrottler::class)->setTarget($this); } } diff --git a/tests/Controllers/ThrottlesRequestTest.php b/tests/Controllers/ThrottlesRequestTest.php deleted file mode 100644 index 35e481a..0000000 --- a/tests/Controllers/ThrottlesRequestTest.php +++ /dev/null @@ -1,62 +0,0 @@ -checkThrottle($request); - - static::$key = $this->throttleKey($request); - - $this->incrementAttempts($request); - - return 'ok'; - } - - public function testOkWithHit(Request $request) - { - $this->checkThrottle(); - - static::$key = $this->throttleKey($request); - - $this->incrementAttempts($request); - - return 'ok'; - } - - public function fireThrottledEvent() - { - static::$fired = true; - } - }; - - $this->instance('test-controller', $controller); - - Route::get('test-ok', 'test-controller@testOk'); - - $this->get('test-ok')->assertSee('ok'); - - Cache::put($controller::$key, 10, 60); - $this->assertFalse($controller::$fired); - - $this->get('test-ok')->assertStatus(302); - $this->assertTrue($controller::$fired); - } -} diff --git a/tests/Eloquent/Casts/CastEnumerableTest.php b/tests/Eloquent/Casts/CastEnumerableTest.php new file mode 100644 index 0000000..1df58c5 --- /dev/null +++ b/tests/Eloquent/Casts/CastEnumerableTest.php @@ -0,0 +1,84 @@ +afterApplicationCreated(function () { + Schema::create('test', function (Blueprint $table) { + $table->id(); + $table->string('castable')->nullable(); + $table->timestamps(); + }); + }); + + $this->model = new class extends Model { + protected $table = 'test'; + + protected $casts = ['castable' => TestCastEnumerable::class]; + }; + + parent::setUp(); + } + + public function test_casts_enum() + { + $this->assertNull($this->model->castable->current()); + + $this->model->castable = 'foo'; + + $this->assertSame('foo', $this->model->castable->current()); + + $this->model->save(); + + $instance = $this->model->find(1); + + $this->assertInstanceOf(CastEnumerable::class, $instance->castable); + $this->assertSame('foo', $instance->castable->current()); + } + + public function test_cast_enum_with_initial() + { + $this->model = new class extends Model { + protected $table = 'test'; + + protected $casts = ['castable' => TestCastEnumerableInitial::class]; + }; + + $this->assertSame('foo', $this->model->castable->current()); + + $this->model->castable = 'bar'; + + $this->assertSame('bar', $this->model->castable->current()); + + $this->model->save(); + + $instance = $this->model->find(1); + + $this->assertInstanceOf(CastEnumerable::class, $instance->castable); + $this->assertSame('bar', $instance->castable->current()); + } +} + +class TestCastEnumerable extends CastEnumerable +{ + protected $states = ['foo', 'bar', 'quz']; +} + +class TestCastEnumerableInitial extends CastEnumerable +{ + protected $states = ['foo', 'bar', 'quz']; + protected $current = 'foo'; +} diff --git a/tests/Eloquent/Casts/CastRepositoryTest.php b/tests/Eloquent/Casts/CastRepositoryTest.php new file mode 100644 index 0000000..315b46a --- /dev/null +++ b/tests/Eloquent/Casts/CastRepositoryTest.php @@ -0,0 +1,86 @@ +afterApplicationCreated(function () { + Schema::create('test', function (Blueprint $table) { + $table->id(); + $table->json('castable')->nullable(); + $table->timestamps(); + }); + }); + + $this->model = new class extends Model { + protected $table = 'test'; + + protected $casts = [ + 'castable' => CastRepository::class, + ]; + }; + + parent::setUp(); + } + + public function test_casts_repository_from_array() + { + $this->model->setAttribute('castable', [ + 'foo' => [ + 'bar' => [ + 'quz', + ], + 'qux' => [ + 'quuz', + 'quux', + ], + ], + ])->save(); + + $instance = $this->model->find(1); + + $this->assertInstanceOf(Repository::class, $instance->castable); + $this->assertSame(['quuz', 'quux'], $instance->castable->get('foo.qux')); + } + + public function test_doesnt_casts_when_value_is_valid_json() + { + $this->model->setAttribute('castable', json_encode(['foo' => ['bar', 'quz']]))->save(); + + $instance = $this->model->find(1); + + $this->assertInstanceOf(Repository::class, $instance->castable); + $this->assertSame(['bar', 'quz'], $instance->castable->get('foo')); + } + + public function test_casts_empty_value_into_empty_repository() + { + $this->model->save(); + + $instance = $this->model->find(1); + + $this->assertInstanceOf(Repository::class, $instance->castable); + $this->assertEmpty($instance->castable->all()); + } + + public function test_casts_simple_string_if_string_invalid() + { + $this->model->setAttribute('castable', 'INVALID')->save(); + + $instance = $this->model->find(1); + + $this->assertInstanceOf(Repository::class, $instance->castable); + $this->assertSame(['INVALID'], $instance->castable->all()); + } +} diff --git a/tests/Models/DefaultColumnsTest.php b/tests/Eloquent/DefaultColumnsTest.php similarity index 78% rename from tests/Models/DefaultColumnsTest.php rename to tests/Eloquent/DefaultColumnsTest.php index b0ac3fa..d34d9b2 100644 --- a/tests/Models/DefaultColumnsTest.php +++ b/tests/Eloquent/DefaultColumnsTest.php @@ -1,6 +1,6 @@ assertNotNull($model->quz); }); } + + public function test_without_default_columns() + { + $model = new class extends Model { + use DefaultColumns; + + protected $table = 'test_table'; + }; + + $model->withoutDefaultColumns()->get()->each(function ($model) { + $this->assertNotNull($model->foo); + $this->assertNotNull($model->qux); + $this->assertNotNull($model->bar); + $this->assertNotNull($model->quz); + }); + } } diff --git a/tests/Models/AutoFillTest.php b/tests/Eloquent/FillsAttributesTest.php similarity index 76% rename from tests/Models/AutoFillTest.php rename to tests/Eloquent/FillsAttributesTest.php index abea92b..883fde3 100644 --- a/tests/Models/AutoFillTest.php +++ b/tests/Eloquent/FillsAttributesTest.php @@ -1,22 +1,26 @@ 'ok', + ]; protected function autoFillable() { - return ['foo', 'quz']; + return ['foo', 'quz', 'quux']; } protected function fillFooAttribute() @@ -36,14 +40,14 @@ protected function fillNotAttribute() }; $this->assertEquals([ - 'foo' => 'bar', 'quz' => 'qux' + 'foo' => 'bar', 'quz' => 'qux', 'quux' => 'ok' ], $model->getAttributes()); } - public function testAutofillModelWithProperty() + public function test_autofill_model_with_property() { $model = new class extends Model { - use AutoFill; + use FillsAttributes; protected $autoFillable = ['foo', 'quz']; @@ -68,12 +72,12 @@ protected function fillNotAttribute() ], $model->getAttributes()); } - public function testAutofillFailsIfNoFillerIsSet() + public function test_autofill_fails_if_no_filler_is_set() { $this->expectException(BadMethodCallException::class); $model = new class extends Model { - use AutoFill; + use FillsAttributes; protected $autoFillable = ['bar', 'quz']; diff --git a/tests/Models/HasSlugTest.php b/tests/Eloquent/HasSlugTest.php similarity index 56% rename from tests/Models/HasSlugTest.php rename to tests/Eloquent/HasSlugTest.php index a85f01c..613cae6 100644 --- a/tests/Models/HasSlugTest.php +++ b/tests/Eloquent/HasSlugTest.php @@ -1,12 +1,11 @@ increments('id'); - $table->string('name'); + $table->string('title'); $table->string('slug'); $table->timestamps(); }); @@ -45,30 +44,20 @@ public function testRoutesBySlug() Carbon::setTestNow($now = Carbon::create(2020, 1, 1)); TestSlugableFooModel::create([ - 'name' => $fooName = 'This Is A Test', + 'title' => $fooName = 'This Is A Test', ]); $this->app['router']->get('foo/{foo}', function (TestSlugableFooModel $foo) { return $foo; })->middleware('bindings'); - if (Str::startsWith(Application::VERSION, '7')) { - $this->get('foo/this-is-a-test')->assertExactJson([ - 'id' => 1, - 'name' => $fooName, - 'slug' => Str::slug($fooName), - 'created_at' => $now->toIso8601ZuluString('microseconds'), - 'updated_at' => $now->toIso8601ZuluString('microseconds'), - ]); - } else { - $this->get('foo/this-is-a-test')->assertExactJson([ - 'id' => 1, - 'name' => $fooName, - 'slug' => Str::slug($fooName), - 'created_at' => $now->toDateTimeString(), - 'updated_at' => $now->toDateTimeString(), - ]); - } + $this->get('foo/this-is-a-test')->assertExactJson([ + 'id' => 1, + 'title' => $fooName, + 'slug' => Str::slug($fooName), + 'created_at' => $now->toIso8601ZuluString('microseconds'), + 'updated_at' => $now->toIso8601ZuluString('microseconds'), + ]); $this->get('foo/notfound')->assertNotFound(); } @@ -85,23 +74,13 @@ public function testRoutesByPersonalizedSlug() return $bar; })->middleware('bindings'); - if (Str::startsWith(Application::VERSION, '7')) { - $this->get('bar/what-happened')->assertExactJson([ - 'id' => 1, - 'quz' => $barName, - 'qux' => Str::slug($barName), - 'created_at' => $now->toIso8601ZuluString('microseconds'), - 'updated_at' => $now->toIso8601ZuluString('microseconds'), - ]); - } else { - $this->get('bar/what-happened')->assertExactJson([ - 'id' => 1, - 'quz' => $barName, - 'qux' => Str::slug($barName), - 'created_at' => $now->toDateTimeString(), - 'updated_at' => $now->toDateTimeString(), - ]); - } + $this->get('bar/what-happened')->assertExactJson([ + 'id' => 1, + 'quz' => $barName, + 'qux' => Str::slug($barName), + 'created_at' => $now->toIso8601ZuluString('microseconds'), + 'updated_at' => $now->toIso8601ZuluString('microseconds'), + ]); $this->get('bar/notfound')->assertNotFound(); } @@ -113,7 +92,7 @@ class TestSlugableFooModel extends Model protected $table = 'foo'; - protected $fillable = ['name']; + protected $fillable = ['title']; } class TestSlugableBarModel extends Model diff --git a/tests/Models/ModelTypeTest.php b/tests/Eloquent/ModelTypeTest.php similarity index 90% rename from tests/Models/ModelTypeTest.php rename to tests/Eloquent/ModelTypeTest.php index d94c416..397d7bf 100644 --- a/tests/Models/ModelTypeTest.php +++ b/tests/Eloquent/ModelTypeTest.php @@ -1,6 +1,6 @@ forceFill([ 'name' => 'foo', @@ -41,10 +41,10 @@ public function testExtendedModelFiltersType() $this->assertCount(1, $models); $this->assertEquals('foo', $models->first()->name); - $this->assertEquals('extended-type-test-model', $models->first()->type); + $this->assertEquals('extended_type_test_model', $models->first()->type); } - public function testExtendedModelOverridesDefaults() + public function test_extended_model_overrides_defaults() { CustomTypeTestModel::make()->forceFill([ 'name' => 'foo' diff --git a/tests/Eloquent/NeighbourRecordsTest.php b/tests/Eloquent/NeighbourRecordsTest.php new file mode 100644 index 0000000..570a018 --- /dev/null +++ b/tests/Eloquent/NeighbourRecordsTest.php @@ -0,0 +1,125 @@ +afterApplicationCreated(function () { + + Schema::create('foo', function (Blueprint $table) { + $table->id(); + $table->string('primary'); + $table->string('random'); + $table->timestamps(); + }); + + $this->model = new class () extends Model { + use NeighbourRecords; + protected $table = 'foo'; + protected $fillable = ['primary', 'random']; + }; + + $now = now(); + + foreach (['foo', 'bar', 'quz', 'qux'] as $item) { + Carbon::setTestNow($now->addSecond()); + $this->model->create([ + 'primary' => $item, + 'random' => Str::random(16), + ]); + } + + Carbon::setTestNow(); + }); + + parent::setUp(); + } + + public function test_neighbour_records() + { + $current = $this->model->find(2); + + $this->assertEquals(1, $current->prevRecord()->getKey()); + $this->assertEquals(3, $current->nextRecord()->getKey()); + + $current = $this->model->find(1); + + $this->assertNull($current->prevRecord()); + $this->assertEquals(2, $current->nextRecord()->getKey()); + + $current = $this->model->find(4); + + $this->assertEquals(3, $current->prevRecord()->getKey()); + $this->assertNull($current->nextRecord()); + } + + public function test_filters_query() + { + Schema::create('bar', function (Blueprint $table) { + $table->id(); + $table->string('primary'); + $table->string('random'); + $table->timestamps(); + }); + + $this->model = new class () extends Model { + use NeighbourRecords; + protected $table = 'bar'; + protected $fillable = ['primary', 'random']; + protected function filterNeighbourQuery($builder) + { + return $builder->where('random', 'group-bar'); + } + }; + + $now = now(); + + foreach (['foo', 'bar', 'quz', 'qux'] as $item) { + Carbon::setTestNow($now->addSecond()); + $this->model->create([ + 'primary' => $item, + 'random' => 'group-foo', + ]); + } + + foreach (['foo', 'bar', 'quz', 'qux'] as $item) { + Carbon::setTestNow($now->addSecond()); + $this->model->create([ + 'primary' => $item, + 'random' => 'group-bar', + ]); + } + + Carbon::setTestNow(); + + $current = $this->model->find(6); + + $this->assertEquals(5, $current->prevRecord()->getKey()); + $this->assertEquals(7, $current->nextRecord()->getKey()); + + $current = $this->model->find(5); + + $this->assertNull($current->prevRecord()); + $this->assertEquals(6, $current->nextRecord()->getKey()); + + $current = $this->model->find(8); + + $this->assertEquals(7, $current->prevRecord()->getKey()); + $this->assertNull($current->nextRecord()); + } +} diff --git a/tests/Models/UsesUuidTest.php b/tests/Eloquent/UsesUuidTest.php similarity index 97% rename from tests/Models/UsesUuidTest.php rename to tests/Eloquent/UsesUuidTest.php index 4a6abc4..bfb0355 100644 --- a/tests/Models/UsesUuidTest.php +++ b/tests/Eloquent/UsesUuidTest.php @@ -1,6 +1,6 @@ assertInstanceOf(Uuid::class, $model->uuid); } - public function testUuidScope() + public function test_uuid_scope() { Schema::create('test_table', function (Blueprint $blueprint) { $blueprint->increments('id'); diff --git a/tests/EnumerableStatesTest.php b/tests/EnumerableStatesTest.php index 9e30fc1..f1c9d40 100644 --- a/tests/EnumerableStatesTest.php +++ b/tests/EnumerableStatesTest.php @@ -2,20 +2,18 @@ namespace Tests; +use LogicException; use Orchestra\Testbench\TestCase; use DarkGhostHunter\Laratraits\EnumerableStates; class EnumerableStatesTest extends TestCase { - public function test_has_states_with_method() + public function test_has_states_with_const() { $class = new class { use EnumerableStates; - protected function states() - { - return ['foo', 'bar']; - } + protected const STATES = ['foo', 'bar']; }; $this->assertNull($class->current()); @@ -23,12 +21,15 @@ protected function states() $this->assertEquals('foo', $class->current()); } - public function test_has_states_with_const() + public function test_has_states_with_method_override() { $class = new class { use EnumerableStates; - protected const STATES = ['foo', 'bar']; + public function getEnumerableStates() + { + return ['foo', 'bar']; + } }; $this->assertNull($class->current()); @@ -38,7 +39,7 @@ public function test_has_states_with_const() public function test_exception_when_invalid_state() { - $this->expectException(\LogicException::class); + $this->expectException(LogicException::class); $class = new class { use EnumerableStates; @@ -56,7 +57,7 @@ public function test_initial_const_method() protected const STATES = ['foo', 'bar']; - protected function initial() + protected function getEnumerableInitialState() { return 'foo'; } @@ -80,7 +81,7 @@ public function test_initial_state_const() public function test_exception_when_class_has_no_states() { - $this->expectException(\LogicException::class); + $this->expectException(LogicException::class); $class = new class { use EnumerableStates; diff --git a/tests/EnumerableTest.php b/tests/EnumerableTest.php index ba1f382..e275c59 100644 --- a/tests/EnumerableTest.php +++ b/tests/EnumerableTest.php @@ -52,7 +52,7 @@ public function test_creates_custom_enumerable_from_initial_state() protected $states = ['foo', 'bar', 'quz']; }; - $bar = $enum::as('foo'); + $bar = call_user_func([$enum, 'as'], 'foo'); $this->assertEquals('foo', $bar->current()); @@ -82,20 +82,6 @@ public function getIterator() $this->assertEquals(['foo', 'bar', 'quz'], $enum->states()); } - public function test_checks_one_or_many_states_possible() - { - $states = ['foo', 'bar', 'quz']; - - $enum = new Enumerable($states); - - $this->assertTrue($enum->has('foo')); - $this->assertTrue($enum->has('foo,bar')); - $this->assertTrue($enum->has(['foo', 'bar'])); - $this->assertFalse($enum->has('qux')); - $this->assertFalse($enum->has('foo,qux')); - $this->assertFalse($enum->has(['foo,qux'])); - } - public function test_returns_current_state() { $states = ['foo', 'bar', 'quz']; @@ -128,7 +114,7 @@ public function test_returns_current_state_as_null_when_uninitialized() public function test_exception_when_sets_invalid_state() { $this->expectException(LogicException::class); - $this->expectExceptionMessage('The state [qux] doesn\'t exists in this Enumerate instance.'); + $this->expectExceptionMessage('The state \'qux\' doesn\'t exists in this Enumerate instance.'); $enum = new Enumerable(['foo', 'bar', 'quz']); diff --git a/tests/FiresItselfTest.php b/tests/FiresItselfTest.php new file mode 100644 index 0000000..273c136 --- /dev/null +++ b/tests/FiresItselfTest.php @@ -0,0 +1,54 @@ +assertDispatched(get_class($class), function ($event) { + return 'fired' === $event::$payload; + }); + } + + public function test_fires_halted() + { + $event = Event::fake(); + + $class = new class ('notfired') { + use FiresItself; + + public static $payload; + + public function __construct($payload) + { + static::$payload = $payload; + } + }; + + call_user_func([get_class($class), 'fireHalted'], 'fired'); + + $event->assertDispatched(get_class($class), function ($event, $payload, $halted = null) { + return 'fired' === $event::$payload && $halted === true; + }); + } +} diff --git a/tests/Middleware/CacheStaticResponseTest.php b/tests/Middleware/CacheStaticResponseTest.php index 522e1ae..25ec9fa 100644 --- a/tests/Middleware/CacheStaticResponseTest.php +++ b/tests/Middleware/CacheStaticResponseTest.php @@ -15,7 +15,7 @@ class CacheStaticResponseTest extends TestCase { - public function testCachesStaticResponse() + public function test_caches_static_response() { $controller = new class { @@ -44,7 +44,7 @@ public function show() $this->assertFalse($controller::$hits); } - public function testUsesDifferentTtlAndStore() + public function test_uses_different_ttl_and_store() { $controller = new class { diff --git a/tests/Middleware/ShareAuthenticatedUserTest.php b/tests/Middleware/ShareAuthenticatedUserTest.php index 1fbbaab..dadf2e6 100644 --- a/tests/Middleware/ShareAuthenticatedUserTest.php +++ b/tests/Middleware/ShareAuthenticatedUserTest.php @@ -10,7 +10,7 @@ class ShareAuthenticatedUserTest extends TestCase { - public function testSharesVerifiedUserInViews() + public function test_shares_verified_user_in_views() { $controller = new class { diff --git a/tests/Middleware/ValidateConsumableSignatureTest.php b/tests/Middleware/ValidateConsumableSignatureTest.php index 8409eeb..4bf68bf 100644 --- a/tests/Middleware/ValidateConsumableSignatureTest.php +++ b/tests/Middleware/ValidateConsumableSignatureTest.php @@ -13,7 +13,7 @@ class ValidateConsumableSignatureTest extends TestCase { - public function testSignatureOnlyUsedOnce() + public function test_signature_only_used_once() { $controller = new class { diff --git a/tests/Models/DynamicallyMutatesTest.php b/tests/Models/DynamicallyMutatesTest.php deleted file mode 100644 index 07bb7b5..0000000 --- a/tests/Models/DynamicallyMutatesTest.php +++ /dev/null @@ -1,123 +0,0 @@ -attributes = [ - 'int' => '1.24', - 'integer' => '2.00', - - 'real' => '3.54', - 'float' => '4.54', - 'double' => '5.54', - - 'decimal:2' => '123.123123', - - 'string' => 2, - - 'bool' => '1', - 'boolean' => '0', - - 'object' => json_encode(['foo' => 'bar', 'quz' => 'qux']), - - 'array' => json_encode(['foo' => 'bar', 'quz' => 'qux']), - 'json' => json_encode(['foo' => 'bar', 'quz' => 'qux']), - - 'collection' => json_encode(['foo' => 'bar', 'quz' => 'qux']), - - 'date' => '2020-09-30', - - 'datetime' => '2020-09-30 14:53:03', - 'custom_datetime' => '2020-09-31 14:53:03', - - 'timestamp' => '2020-09-31 14:53:03', - - 'null' => null, - - 'foo' => 'bar', - - 'value' => '', - 'type' => '', - ]; - } - - public function castTo($attribute) { - $this->attributes['value'] = $this->attributes[$attribute]; - $this->attributes['type'] = $attribute; - - return $this->castAttributeInto('value', 'type'); - } - }; - - $this->assertIsInt($model->castTo('int')); - $this->assertEquals(1, $model->castTo('int')); - - $this->assertIsInt($model->castTo('integer')); - $this->assertEquals(2, $model->castTo('integer')); - - $this->assertIsFloat($model->castTo('real')); - $this->assertEquals(3.54, $model->castTo('real')); - $this->assertIsFloat($model->castTo('float')); - $this->assertEquals(4.54, $model->castTo('float')); - $this->assertIsFloat($model->castTo('double')); - $this->assertEquals(5.54, $model->castTo('double')); - - $this->assertIsString($model->castTo('decimal:2')); - $this->assertEquals('123.12', $model->castTo('decimal:2')); - - $this->assertIsString($model->castTo('string')); - $this->assertEquals('2', $model->castTo('string')); - - $this->assertIsBool($model->castTo('bool')); - $this->assertTrue($model->castTo('bool')); - $this->assertIsBool($model->castTo('boolean')); - $this->assertFalse($model->castTo('boolean')); - - $this->assertIsObject($model->castTo('object')); - $this->assertEquals('bar', $model->castTo('object')->foo); - $this->assertEquals('qux', $model->castTo('object')->quz); - - $this->assertIsArray($model->castTo('array')); - $this->assertEquals('bar', $model->castTo('array')['foo']); - $this->assertEquals('qux', $model->castTo('array')['quz']); - - $this->assertIsArray($model->castTo('json')); - $this->assertEquals('bar', $model->castTo('json')['foo']); - $this->assertEquals('qux', $model->castTo('json')['quz']); - - $this->assertInstanceOf(Collection::class, $model->castTo('collection')); - $this->assertEquals('bar', $model->castTo('collection')['foo']); - $this->assertEquals('qux', $model->castTo('collection')['quz']); - - $this->assertInstanceOf(Carbon::class, $model->castTo('date')); - $this->assertEquals('2020-09-30', $model->castTo('date')->toDateString()); - - $this->assertInstanceOf(Carbon::class, $model->castTo('datetime')); - $this->assertEquals('2020-09-30 14:53:03', $model->castTo('datetime')->toDateTimeString()); - $this->assertInstanceOf(Carbon::class, $model->castTo('custom_datetime')); - $this->assertEquals('2020-10-01 14:53:03', $model->castTo('custom_datetime')->toDateTimeString()); - - $this->assertIsInt($model->castTo('timestamp')); - $this->assertEquals(1601563983, $model->castTo('timestamp')); - - $this->assertNull($model->castTo('null')); - - $this->assertEquals('bar', $model->castTo('foo')); - } -} diff --git a/tests/Models/HasFileTest.php b/tests/Models/HasFileTest.php deleted file mode 100644 index fa90160..0000000 --- a/tests/Models/HasFileTest.php +++ /dev/null @@ -1,254 +0,0 @@ -set('filesystems.disks.test', [ - 'driver' => 'local', - 'root' => storage_path('app/test'), - ]); - } - - protected function setUp() : void - { - $this->afterApplicationCreated(function () { - - Schema::create('foo', function (Blueprint $table) { - $table->increments('id'); - $table->string('disk'); - $table->string('path'); - $table->string('file_hash'); - $table->timestamps(); - }); - - Schema::create('bar', function (Blueprint $table) { - $table->increments('id'); - $table->string('test_disk'); - $table->string('test_path'); - $table->string('test_file_hash'); - $table->timestamps(); - }); - - Storage::disk()->delete('test_path.txt'); - Storage::disk('test')->delete('test_path.txt'); - }); - - parent::setUp(); - } - - public function testSetsAndSavesFile() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - $model->saveFileContents(); - - $this->assertStringEqualsFile(storage_path('app/test_path.txt'), $string); - } - - public function testSavesFile() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $model->saveFileContents($string = Str::random()); - - $this->assertStringEqualsFile(storage_path('app/test_path.txt'), $string); - } - - public function testSavesFileWhenSavingModel() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - - $model->save(); - - $this->assertStringEqualsFile(storage_path('app/test_path.txt'), $string); - } - - public function testSavesFileWhenSavingModelEvenIfModelHasNoChanges() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - - $model->save(); - - $model = TestFileModel::first(); - - $model->setFileContents($string = Str::random()); - - $model->save(); - - $this->assertStringEqualsFile(storage_path('app/test_path.txt'), $string); - } - - public function testSavesDiskAndPathWhenSavingModel() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - - $model->save(); - - $this->assertEquals('local', $model->disk); - $this->assertEquals('test_path.txt', $model->path); - } - - public function testCantSaveWithoutPath() - { - $this->expectException(\LogicException::class); - - $model = new TestFileModel; - $model->setFileContents($string = Str::random()); - - $model->save(); - - $this->assertEquals('local', $model->disk); - $this->assertEquals('test_path.txt', $model->path); - } - - public function testRetrievesFile() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - - $model->save(); - - $model = TestFileModel::first(); - - $this->assertEquals($string, $model->getFileContents()); - } - - public function testUsesCustomDisk() - { - $model = new TestFileModel; - $model->setAttribute('disk', 'test'); - $model->setAttribute('path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - - $model->save(); - - $model = TestFileModel::first(); - - $this->assertEquals($string, $model->getFileContents()); - } - - public function testDoesntSavesFileIfNotChanged() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $hash = $model->saveFileContents($string = Str::random()); - - $model->save(); - - /** @var \Illuminate\Contracts\Filesystem\Filesystem $storage */ - $storage = Storage::fake('local'); - - $model = TestFileModel::first(); - - $model->setFileContents($string); - - $this->assertTrue($model->saveFileContents()); - - $this->assertEmpty($storage->files('/')); - } - - public function testDoesntSavesWhenSavingModelIfNotChanged() - { - $model = new TestFileModel; - $model->setAttribute('path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - - $model->save(); - - /** @var \Illuminate\Contracts\Filesystem\Filesystem $storage */ - $storage = Storage::fake('local'); - - $model = TestFileModel::first(); - - $this->assertTrue($model->save()); - - $this->assertEmpty($storage->files('/')); - } - - public function testUsesCustomAttributes() - { - $model = new TestCustomFileModel; - $model->setAttribute('test_path', 'test_path.txt'); - $model->setFileContents($string = Str::random()); - - $model->save(); - - $model = TestCustomFileModel::first(); - - $this->assertEquals($string, $model->getFileContents()); - } - - public function testDetectsFileChanges() - { - $model = new TestCustomFileModel; - $model->setAttribute('test_path', 'test_path.txt'); - $hash = $model->setFileContents($string = Str::random()); - - $model->save(); - - $model = TestCustomFileModel::first(); - - $newHash = $model->setFileContents($string); - - $this->assertEquals($string, $model->getFileContents()); - $this->assertEquals($hash, $newHash); - - $this->assertFalse($model->isFileChanged()); - } - - protected function tearDown() : void - { - Storage::disk()->delete('test_path.txt'); - Storage::disk('test')->delete('test_path.txt'); - - parent::tearDown(); - } -} - -class TestFileModel extends Model -{ - use HasFile; - - protected $table = 'foo'; -} - -class TestCustomFileModel extends Model -{ - use HasFile; - - protected $table = 'bar'; - - protected function getQualifiedFileHashColumn() - { - return 'test_file_hash'; - } - - protected function getQualifiedStorageDiskColumn() - { - return 'test_disk'; - } - - protected function getQualifiedStoragePathColumn() - { - return 'test_path'; - } -} diff --git a/tests/Models/NeighbourRecordsTest.php b/tests/Models/NeighbourRecordsTest.php deleted file mode 100644 index 309e20e..0000000 --- a/tests/Models/NeighbourRecordsTest.php +++ /dev/null @@ -1,65 +0,0 @@ -afterApplicationCreated(function () { - - Schema::create('foo', function (Blueprint $table) { - $table->string('primary'); - $table->string('random'); - $table->timestamps(); - }); - - $this->model = new class () extends Model { - use NeighbourRecords; - protected $table = 'foo'; - protected $primaryKey = 'primary'; - public $incrementing = false; - protected $fillable = ['primary', 'random']; - }; - - foreach (['foo', 'bar', 'quz', 'qux'] as $item) { - $this->model->create([ - 'primary' => $item, - 'random' => Str::random(16), - ]); - } - }); - - parent::setUp(); - } - - public function testNeighbourRecords() - { - $current = $this->model->find('bar'); - - $this->assertEquals('foo', $current->prevRecord()->primary); - $this->assertEquals('quz', $current->nextRecord()->primary); - - $current = $this->model->find('foo'); - - $this->assertNull($current->prevRecord()); - $this->assertEquals('bar', $current->nextRecord()->primary); - - $current = $this->model->find('qux'); - - $this->assertEquals('quz', $current->prevRecord()->primary); - $this->assertNull($current->nextRecord()); - } -} diff --git a/tests/Models/SoftCachesAccessorsTest.php b/tests/Models/SoftCachesAccessorsTest.php deleted file mode 100644 index 63ee6ce..0000000 --- a/tests/Models/SoftCachesAccessorsTest.php +++ /dev/null @@ -1,64 +0,0 @@ - '{"foo":"bar","quz":"qux"}', - 'not' => 'cached' - ]; - - protected function getJsonAttribute($json) - { - ++static::$hits; - - return Collection::make(json_decode($json, true)); - } - - protected function getNotAttribute($value) - { - return 'yes'; - } - }; - - $this->assertEquals('yes', $model->not); - - $this->assertInstanceOf(Collection::class, $model->json); - $this->assertEquals(1, $model::$hits); - - $model->json; - - $this->assertEquals(1, $model::$hits); - - $model->flushAccessorsCache(); - - $model->json; - - $this->assertEquals(2, $model::$hits); - - $this->assertInstanceOf( - Collection::class,$model->getAttributeWithoutCache('json') - ); - $this->assertEquals(3, $model::$hits); - - $model->withoutMutatorCache(function ($model) { - $model->json; - }); - $this->assertEquals(4, $model::$hits); - } -} diff --git a/tests/MultitapsTest.php b/tests/MultitapsTest.php index 55c9e6d..59c8086 100644 --- a/tests/MultitapsTest.php +++ b/tests/MultitapsTest.php @@ -7,7 +7,7 @@ class MultitapsTest extends TestCase { - public function testMultitaps() + public function test_multitaps() { $multitapable = new class() { use Multitaps; @@ -33,7 +33,7 @@ public function qux() $this->assertEquals('qux', $multitapable->multitap()->bar()->quz()->qux()->target->foo); } - public function testMultitapsWithClosure() + public function test_multitaps_with_closure() { $multitapable = new class() { use Multitaps; @@ -53,7 +53,7 @@ public function qux() $this->assertEquals('qux', $result); } - public function testMultitapsAndUntaps() + public function test_multitaps_and_untaps() { $multitapable = new class() { use Multitaps; diff --git a/tests/PipesThroughTest.php b/tests/PipesThroughTest.php index e9b2b7b..5b61238 100644 --- a/tests/PipesThroughTest.php +++ b/tests/PipesThroughTest.php @@ -11,7 +11,7 @@ class PipesThroughTest extends TestCase { - public function testPipesThroughDefaultPipelines() + public function test_pipes_through_default_pipelines() { $pipes = new class() { use PipesThrough; @@ -26,7 +26,7 @@ public function testPipesThroughDefaultPipelines() $this->assertEquals('bar', $pipes->pipe($pipe)->foo); } - public function testPipesCustomPipeline() + public function test_pipes_custom_pipeline() { $pipeline = new class() extends Pipeline { public function __construct(Container $container = null) @@ -57,7 +57,7 @@ protected function makePipeline() : \Illuminate\Contracts\Pipeline\Pipeline $this->assertEquals('bar', $pipes->pipe()->foo); } - public function testPipesToClosureDestination() + public function test_pipes_to_closure_destination() { $pipes = new class() { use PipesThrough; @@ -77,7 +77,7 @@ public function testPipesToClosureDestination() $this->assertEquals('quz', $pipes->pipe($pipe, $destination)->foo); } - public function testDispatchesToQueue() + public function test_dispatches_to_queue() { $bus = Bus::fake(); @@ -91,7 +91,7 @@ public function testDispatchesToQueue() $bus->assertDispatched(DispatchablePipeline::class); } - public function testDispatchesToQueueWithPipes() + public function test_dispatches_to_queue_with_pipes() { $bus = Bus::fake(); @@ -102,12 +102,12 @@ public function testDispatchesToQueueWithPipes() $done = false; - $pipes->dispatchPipeline([ + $pipes->dispatchPipeline( function ($thing, $next) use (&$done) { $done = true; return $next($thing); } - ]); + ); $bus->assertDispatched(DispatchablePipeline::class, function ($job) { $job->handle(); diff --git a/tests/RendersFromMarkdownTest.php b/tests/RendersFromMarkdownTest.php index 4a43f80..8e8716c 100644 --- a/tests/RendersFromMarkdownTest.php +++ b/tests/RendersFromMarkdownTest.php @@ -9,7 +9,7 @@ class RendersFromMarkdownTest extends TestCase { - public function testRenders() + public function test_renders() { $htmlable = new class() { use RendersFromMarkdown; @@ -26,7 +26,7 @@ protected function getMarkdown() $this->assertStringContainsString('

foo, bar

', $html); } - public function testRendersEmptyString() + public function test_renders_empty_string() { $htmlable = new class() { use RendersFromMarkdown; @@ -43,7 +43,7 @@ protected function getMarkdown() $this->assertStringContainsString('', $html); } - public function testRendersMultipleLinesFromArray() + public function test_renders_multiple_lines_from_array() { $htmlable = new class() { use RendersFromMarkdown; @@ -61,7 +61,7 @@ protected function getMarkdown() $this->assertStringContainsString('bar', $html); } - public function testRendersMultipleLinesFromCollection() + public function test_renders_multiple_lines_from_collection() { $htmlable = new class() { use RendersFromMarkdown; @@ -79,7 +79,7 @@ protected function getMarkdown() $this->assertStringContainsString('bar', $html); } - public function testRendersAsString() + public function test_renders_as_string() { $htmlable = new class() { use RendersFromMarkdown; @@ -93,7 +93,7 @@ protected function getMarkdown() $this->assertStringContainsString('

foobar

', (string)$htmlable); } - public function testRendersAsHtml() + public function test_renders_as_html() { $htmlable = new class() implements Htmlable { use RendersFromMarkdown; diff --git a/tests/SavesToCacheTest.php b/tests/SavesToCacheTest.php index 3a6decb..57d0235 100644 --- a/tests/SavesToCacheTest.php +++ b/tests/SavesToCacheTest.php @@ -4,18 +4,15 @@ use Mockery; use LogicException; -use JsonSerializable; use Illuminate\Cache\Repository; use Orchestra\Testbench\TestCase; use Illuminate\Support\Facades\Cache; -use Illuminate\Contracts\Support\Jsonable; -use Illuminate\Contracts\Support\Htmlable; use DarkGhostHunter\Laratraits\SavesToCache; use Illuminate\Contracts\Cache\Repository as RepositoryContract; class SavesToCacheTest extends TestCase { - public function testSavesToCache() + public function test_saves_to_cache() { $cacheable = new class() { use SavesToCache; @@ -31,7 +28,7 @@ protected function toCache() $this->assertEquals('bar', Cache::get('foo')); } - public function testSavesWithDefaultKey() + public function test_saves_with_default_key() { $cacheable = new class() { use SavesToCache; @@ -52,7 +49,7 @@ protected function toCache() $this->assertEquals('bar', Cache::get('foo')); } - public function testSavesWithDefaultTtl() + public function test_saves_with_default_ttl() { $store = $this->instance(RepositoryContract::class, Mockery::mock(Repository::class)); @@ -76,58 +73,7 @@ protected function toCache() $this->assertTrue($cacheable->saveToCache('foo')); } - public function testSavesJsonable() - { - $cacheable = new class() implements Jsonable { - use SavesToCache; - - /** - * @inheritDoc - */ - public function toJson($options = 0) - { - return '{"foo":"bar"}'; - } - }; - - $cacheable->saveToCache('foo'); - - $this->assertEquals('{"foo":"bar"}', Cache::get('foo')); - } - - public function testSavesJsonSerializable() - { - $cacheable = new class() implements JsonSerializable { - use SavesToCache; - - public function jsonSerialize() - { - return ['foo' => 'bar']; - } - }; - - $cacheable->saveToCache('foo'); - - $this->assertEquals('{"foo":"bar"}', Cache::get('foo')); - } - - public function testSavesHtmlable() - { - $cacheable = new class() implements Htmlable { - use SavesToCache; - - public function toHtml() - { - return 'bar'; - } - }; - - $cacheable->saveToCache('foo'); - - $this->assertEquals('bar', Cache::get('foo')); - } - - public function testSavesStringable() + public function test_saves_stringable() { $cacheable = new class() { use SavesToCache; @@ -143,7 +89,7 @@ public function __toString() $this->assertEquals('bar', Cache::get('foo')); } - public function testSavesObjectInstance() + public function test_saves_object_instance() { $store = $this->instance(RepositoryContract::class, Mockery::mock(Repository::class)); @@ -162,7 +108,7 @@ public function testSavesObjectInstance() $cacheable->saveToCache('foo'); } - public function testExceptionWhenNoDefaultKey() + public function test_exception_when_no_default_key() { $this->expectException(LogicException::class); diff --git a/tests/SavesToSessionTest.php b/tests/SavesToSessionTest.php index 79bbe94..8da3826 100644 --- a/tests/SavesToSessionTest.php +++ b/tests/SavesToSessionTest.php @@ -3,17 +3,14 @@ namespace DarkGhostHunter\Laratraits\Tests; use LogicException; -use JsonSerializable; use Orchestra\Testbench\TestCase; use Illuminate\Support\Facades\Session; -use Illuminate\Contracts\Support\Jsonable; -use Illuminate\Contracts\Support\Htmlable; use DarkGhostHunter\Laratraits\SavesToSession; use Illuminate\Contracts\Session\Session as SessionContract; class SavesToSessionTest extends TestCase { - public function testSavesToSession() + public function test_saves_to_session() { $sessionable = new class() { use SavesToSession; @@ -29,58 +26,7 @@ public function toSession() $this->assertEquals('bar', Session::get('foo')); } - public function testSavesJsonable() - { - $sessionable = new class() implements Jsonable { - use SavesToSession; - - /** - * @inheritDoc - */ - public function toJson($options = 0) - { - return '{"foo":"bar"}'; - } - }; - - $sessionable->saveToSession('foo'); - - $this->assertEquals('{"foo":"bar"}', Session::get('foo')); - } - - public function testSavesJsonSerializable() - { - $sessionable = new class() implements JsonSerializable { - use SavesToSession; - - public function jsonSerialize() - { - return ['foo' => 'bar']; - } - }; - - $sessionable->saveToSession('foo'); - - $this->assertEquals('{"foo":"bar"}', Session::get('foo')); - } - - public function testSavesHtmlable() - { - $sessionable = new class() implements Htmlable { - use SavesToSession; - - public function toHtml() - { - return 'bar'; - } - }; - - $sessionable->saveToSession('foo'); - - $this->assertEquals('bar', Session::get('foo')); - } - - public function testSavesStringable() + public function test_saves_stringable() { $sessionable = new class() { use SavesToSession; @@ -96,7 +42,7 @@ public function __toString() $this->assertEquals('bar', Session::get('foo')); } - public function testSavesObjectInstance() + public function test_saves_object_instance() { $session = new class implements SessionContract { public static $used = false; @@ -125,7 +71,7 @@ public function handlerNeedsRequest(){} public function setRequestOnHandler($request){} }; - $session = $this->app->instance(SessionContract::class, $session); + $session = $this->app->instance('session', $session); $sessionable = new class() { use SavesToSession; @@ -136,7 +82,7 @@ public function setRequestOnHandler($request){} $this->assertTrue($session::$used); } - public function testSavesWithDefaultSessionKey() + public function test_saves_with_default_session_key() { $sessionable = new class() { use SavesToSession; @@ -157,7 +103,7 @@ public function __toString() $this->assertEquals('bar', Session::get('foo')); } - public function testExceptionWhenNoSessionKey() + public function test_exception_when_no_session_key() { $this->expectException(LogicException::class); diff --git a/tests/SavesToStorageTest.php b/tests/SavesToStorageTest.php index 7a4b17d..dfc7394 100644 --- a/tests/SavesToStorageTest.php +++ b/tests/SavesToStorageTest.php @@ -4,18 +4,15 @@ use Mockery; use LogicException; -use JsonSerializable; use Orchestra\Testbench\TestCase; use Illuminate\Support\Facades\Storage; -use Illuminate\Contracts\Support\Jsonable; -use Illuminate\Contracts\Support\Htmlable; use Illuminate\Filesystem\FilesystemAdapter; use DarkGhostHunter\Laratraits\SavesToStorage; use Illuminate\Contracts\Filesystem\Filesystem as FilesystemContract; class SavesToStorageTest extends TestCase { - public function testSavesToStorage() + public function test_saves_to_storage() { $storage = Storage::fake(); @@ -33,67 +30,7 @@ public function toStore() $this->assertTrue($storage->exists('test_path.json')); } - public function testSavesJsonable() - { - $storage = Storage::fake(); - - $storable = new class() implements Jsonable { - use SavesToStorage; - - /** - * @inheritDoc - */ - public function toJson($options = 0) - { - return '{"foo": "bar"}'; - } - }; - - $storable->saveToStore('test_path.json'); - - $this->assertTrue($storage->exists('test_path.json')); - $this->assertJson($storage->get('test_path.json')); - } - - public function testSavesJsonSerializable() - { - $storage = Storage::fake(); - - $storable = new class() implements JsonSerializable { - use SavesToStorage; - - public function jsonSerialize() - { - return ['foo' => 'bar']; - } - }; - - $storable->saveToStore('test_path.json'); - - $this->assertTrue($storage->exists('test_path.json')); - $this->assertJson($storage->get('test_path.json')); - } - - public function testSavesHtmlable() - { - $storage = Storage::fake(); - - $storable = new class() implements Htmlable { - use SavesToStorage; - - public function toHtml() - { - return 'foo'; - } - }; - - $storable->saveToStore('test_path.json'); - - $this->assertTrue($storage->exists('test_path.json')); - $this->assertEquals('foo', $storage->get('test_path.json')); - } - - public function testSavesStringable() + public function test_saves_stringable() { $storage = Storage::fake(); @@ -112,7 +49,7 @@ public function __toString() $this->assertEquals('foo', $storage->get('test_path.json')); } - public function testSavesObjectInstance() + public function test_saves_object_instance() { $storage = $this->instance(FilesystemContract::class, Mockery::mock(FilesystemAdapter::class)); @@ -127,7 +64,7 @@ public function testSavesObjectInstance() $storable->saveToStore('test_path.json'); } - public function testSavesWithDefaultStoragePath() + public function test_saves_with_default_storage_path() { $storage = Storage::fake(); @@ -151,7 +88,7 @@ public function __toString() $this->assertEquals('foo', $storage->get('test_path.json')); } - public function testExceptionWhenNoDefaultPath() + public function test_exception_when_no_default_path() { $this->expectException(LogicException::class); diff --git a/tests/SendsToHttpTest.php b/tests/SendsToHttpTest.php new file mode 100644 index 0000000..18077dd --- /dev/null +++ b/tests/SendsToHttpTest.php @@ -0,0 +1,51 @@ + Http::response([''], 200, ['Headers']), + ]); + + $class = new class { + use SendsToHttp; + + public $url = 'go.com'; + + protected function toHttp() + { + return ['foo' => 'bar']; + } + }; + + /** @var \Illuminate\Http\Client\Response $response */ + $response = $class->send(); + + $this->assertSame(200, $response->status()); + } + + public function test_exception_when_no_url_set() + { + $class = new class { + use SendsToHttp; + + protected function toHttp() + { + return ['foo' => 'bar']; + } + }; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This instance of ' . get_class($class) . ' has no default URL to be sent to.'); + + $class->send(); + } +} diff --git a/tests/ServiceProviders/RegisterBladeExtensionsTest.php b/tests/ServiceProviders/RegisterBladeExtensionsTest.php new file mode 100644 index 0000000..227840d --- /dev/null +++ b/tests/ServiceProviders/RegisterBladeExtensionsTest.php @@ -0,0 +1,59 @@ +app) extends ServiceProvider { + use RegisterBladeExtensions; + + protected $directives = [ + 'testDirective' => 'Tests\ServiceProviders\TestBladeHandler@testDirectiveHandle' + ]; + + protected $if = [ + 'testIf' => 'Tests\ServiceProviders\TestBladeHandler@testIfHandle', + ]; + + protected $include = [ + 'testInclude' => 'testInclude' + ]; + + public function boot() + { + $this->registerBladeExtensions(); + } + }; + + $this->app->register($class); + + /** @var \Illuminate\View\Compilers\BladeCompiler $compiler */ + $compiler = Blade::getFacadeRoot(); + + $this->assertArrayHasKey('testDirective', $compiler->getCustomDirectives()); + $this->assertArrayHasKey('testIf', $compiler->getCustomDirectives()); + $this->assertArrayHasKey('unlesstestIf', $compiler->getCustomDirectives()); + $this->assertArrayHasKey('elsetestIf', $compiler->getCustomDirectives()); + $this->assertArrayHasKey('endtestIf', $compiler->getCustomDirectives()); + $this->assertArrayHasKey('testInclude', $compiler->getCustomDirectives()); + } +} + +class TestBladeHandler +{ + public static function testDirectiveHandle() + { + return 'ok'; + } + public static function testIfHandle() + { + return 'ok'; + } +} diff --git a/tests/ServiceProviders/RegisterGatesTest.php b/tests/ServiceProviders/RegisterGatesTest.php new file mode 100644 index 0000000..3e3537d --- /dev/null +++ b/tests/ServiceProviders/RegisterGatesTest.php @@ -0,0 +1,36 @@ +app) extends ServiceProvider { + use RegisterGates; + + protected $gates = [ + 'view-dashboard' => 'Tests\ServiceProviders\TestGatesHandler@foo', + 'create-users' => 'Tests\ServiceProviders\TestGatesHandler@bar', + ]; + + public function boot() + { + $this->registerGates(); + } + }; + + $this->app->register($class); + + /** @var \Illuminate\Contracts\Auth\Access\Gate $auth */ + $auth = Gate::getFacadeRoot(); + + $this->assertArrayHasKey('view-dashboard', $auth->abilities()); + $this->assertArrayHasKey('create-users', $auth->abilities()); + } +} diff --git a/tests/ServiceProviders/RegisterObserversTest.php b/tests/ServiceProviders/RegisterObserversTest.php new file mode 100644 index 0000000..7630a48 --- /dev/null +++ b/tests/ServiceProviders/RegisterObserversTest.php @@ -0,0 +1,74 @@ +app) extends ServiceProvider { + use RegisterObservers; + + protected $observers = [ + 'Tests\ServiceProviders\TestModelFoo' => 'Tests\ServiceProviders\TestFooObserver', + 'Tests\ServiceProviders\TestModelBar' => [ + 'Tests\ServiceProviders\TestBarObserver', + 'Tests\ServiceProviders\TestQuzObserver', + ], + ]; + + public function boot() + { + $this->registerObservers(); + } + }; + + $this->app->register($class); + + $dispatcher = app(Dispatcher::class); + + $this->assertNotEmpty($dispatcher->getListeners('eloquent.created: Tests\ServiceProviders\TestModelFoo')); + $this->assertNotEmpty($dispatcher->getListeners('eloquent.saved: Tests\ServiceProviders\TestModelBar')); + $this->assertNotEmpty($dispatcher->getListeners('eloquent.deleted: Tests\ServiceProviders\TestModelBar')); + } +} + +class TestModelFoo extends Model +{ + +} + +class TestModelBar extends Model +{ + +} + +class TestFooObserver +{ + public function created() + { + + } +} + +class TestBarObserver +{ + public function saved() + { + + } +} + +class TestQuzObserver +{ + public function deleted() + { + + } +} diff --git a/tests/Stubs/TestDirectory/TestDiscoverableClassQuz.php b/tests/Stubs/TestDirectory/TestDiscoverableClassQuz.php deleted file mode 100644 index 4e740dc..0000000 --- a/tests/Stubs/TestDirectory/TestDiscoverableClassQuz.php +++ /dev/null @@ -1,18 +0,0 @@ -throttle(1, 1)->shouldThrottle(); + $class->throttle(1, 1)->shouldThrottle(); + + $this->assertCount(1, $class::$executed); + } + + public function test_uses_default() + { + $class = new class { + use ThrottleMethods; + + public $used = false; + + public static $executed = []; + + public function shouldThrottle() + { + static::$executed[] = true; + } + }; + + $class->throttle(1, 1, function ($class) { + $class->used = true; + })->shouldThrottle(); + + $this->assertCount(1, $class::$executed); + $this->assertFalse($class->used); + + $class->throttle(1, 1, function ($class) { + $class->used = true; + })->shouldThrottle(); + + $this->assertCount(1, $class::$executed); + $this->assertTrue($class->used); + } + + public function test_clears_throttler() + { + $class = new class { + use ThrottleMethods; + + public static $executed = []; + + public function shouldThrottle() + { + static::$executed[] = true; + } + }; + + $class->throttle(1, 1)->shouldThrottle(); + + $this->assertCount(1, $class::$executed); + + $class->throttleClear('shouldThrottle'); + + $class->throttle(1, 1)->shouldThrottle(); + + $this->assertCount(2, $class::$executed); + } +} diff --git a/tests/ValidatesItselfTest.php b/tests/ValidatesItselfTest.php index 2a36852..93cb509 100644 --- a/tests/ValidatesItselfTest.php +++ b/tests/ValidatesItselfTest.php @@ -9,7 +9,7 @@ class ValidatesItselfTest extends TestCase { - public function testValidatesFromExternalData() + public function test_validates_from_external_data() { $validatable = new class() { use ValidatesItself; @@ -29,7 +29,7 @@ protected function validationRules() : array $this->assertEquals(['foo' => 'bar'], $validated); } - public function testValidatesFromInternalData() + public function test_validates_from_internal_data() { $validatable = new class() { use ValidatesItself; @@ -54,7 +54,7 @@ protected function validationRules() : array $this->assertEquals(['foo' => 'bar'], $validated); } - public function testValidatesExternalDataOverInternalData() + public function test_validates_external_data_over_internal_data() { $validatable = new class() { use ValidatesItself; @@ -81,7 +81,7 @@ protected function validationRules() : array $this->assertEquals(['foo' => 'bar'], $validated); } - public function testValidatesReturnTrueWhenValid() + public function test_validates_return_true_when_valid() { $validatable = new class() { use ValidatesItself; @@ -98,7 +98,7 @@ protected function validationRules() : array $this->assertFalse($validatable->validates(['foo' => 'not_bar'])); } - public function testValidatedReturnsValidatedValues() + public function test_validated_returns_validated_values() { $validatable = new class() { use ValidatesItself; @@ -116,7 +116,7 @@ protected function validationRules() : array $this->assertEquals(['foo' => 'bar'], $validatable->validate(['foo' => 'bar'])); } - public function testPassesValidationMessages() + public function test_passes_validation_messages() { $validatable = new class() { use ValidatesItself; @@ -147,7 +147,7 @@ protected function validationMessages() } } - public function testPassesCustomAttributes() + public function test_passes_custom_attributes() { $validatable = new class() { use ValidatesItself; @@ -178,7 +178,7 @@ protected function customAttributes() } } - public function testPassValidationAfterCallback() + public function test_pass_validation_after_callback() { $validatable = new class() { use ValidatesItself; @@ -205,7 +205,7 @@ protected function validationRules() : array } - public function testExceptionWhenNoDataToValidate() + public function test_exception_when_no_data_to_validate() { $this->expectException(\LogicException::class); From 860f86c546f0ceec09abb73a928103ae3231afc2 Mon Sep 17 00:00:00 2001 From: DarkGhostHunter Date: Mon, 25 May 2020 02:10:33 -0400 Subject: [PATCH 5/6] Updated Upgrade and README.md --- README.md | 14 ++++++++------ UPGRADE.md | 4 +--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bd412ac..ab2a10f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Laratraits is a Laravel package containing useful traits and some classes to use * Laravel 7. * PHP 7.2.15 or later. +## [Upgrade Guide from 1.x to 2.x](UPGRADE.md) + ## Installation Fire up Composer and that's it. @@ -64,7 +66,7 @@ Just remember to **change the namespace** if you're copy-pasting them! * [`SavesToSession`](src/SavesToSession.php): Saves the object (or part of it) to the session. * [`SavesToStorage`](src/SavesToStorage.php): Saves the object (or part of it) to the storage. * [`SendsToHttp`](src/SendsToHttp.php): Sends the object (or part of it) through an HTTP Request. -* [`ThrottleActions`](src/ThrottleMethods.php): Throttles a given method in a class easily. +* [`ThrottleMethods`](src/ThrottleMethods.php): Throttles a given method in a class transparently. * [`ValidatesItself`](src/ValidatesItself.php): Validates an incoming data using self-contained rules. ### Useful classes @@ -73,7 +75,7 @@ Just remember to **change the namespace** if you're copy-pasting them! ### Models -* [`AutoFill`](src/Eloquent/FillsAttributes.php): Automatically fills the Model with values by each method name, like `fillFooAttribute()`. +* [`FillsAttributes`](src/Eloquent/FillsAttributes.php): Automatically fills the Model with values by each method name, like `fillFooAttribute()`. * [`DefaultColumns`](src/Eloquent/DefaultColumns.php): Adds a `DefaultColumns` Global Scope to the Model selecting only given default columns, unless overrun manually in the query. * [`HasSlug`](src/Eloquent/HasSlug.php): Allows a Model to be bound to routes using the slug like `this-is-the-model`. Requires a new column in the table. * [`ModelType`](src/Eloquent/ModelType.php): Useful for Models that share a single table but have different "types", like Publications: Article, Post, Note, etc. @@ -82,18 +84,18 @@ Just remember to **change the namespace** if you're copy-pasting them! #### Casts -* [`CastEnumerable`](src/Eloquent/Casts/CastEnumerable.php): Allows a custom Enumerable class to be [_castable_](https://laravel.com/docs/eloquent-mutators#custom-casts) in a model. -* [`CastsRepository`](src/Eloquent/Casts/CastRepository.php): Allows an array property to be cased as a Repository. +* [`CastEnumerable`](src/Eloquent/Casts/CastEnumerable.php): Allows a string or integer column to be [_casted_](https://laravel.com/docs/eloquent-mutators#custom-casts) as Enumerable inside a model. +* [`CastsRepository`](src/Eloquent/Casts/CastRepository.php): Allows an json column to be [_casted_](https://laravel.com/docs/eloquent-mutators#custom-casts) as a Repository (like a config tree). ### Global Scopes -* [`MacrosEloquent`](src/Scopes/MacrosEloquent.php): Automatically adds selective Macros to the Eloquent Builder instance itself, instead of globally, when using a Global Scope. Append `macro` to a public static method and that's it. +* [`MacrosEloquent`](src/Scopes/MacrosEloquent.php): Automatically adds selective Macros to the Eloquent Builder instance itself, instead of globally, when using a Global Scope. Append `macro` to a public static method and that's it, done. ### Middleware * [`CacheStaticResponse`](src/Middleware/CacheStaticResponse.php): Caches static responses, avoiding running the controller logic, for a given time. * [`ShareAuthenticatedUser`](src/Middleware/ShareAuthenticatedUser.php): Shares the authenticated user across all views. -* [`ValidateConsumableSignature`](src/Middleware/ValidateConsumableSignature.php): Makes [signed routes](https://laravel.com/docs/urls#signed-urls) work only one time except on client or server errors. +* [`ValidateConsumableSignature`](src/Middleware/ValidateConsumableSignature.php): Makes [signed routes](https://laravel.com/docs/urls#signed-urls) work only one time, except on client or server errors. ## Missing a trait? diff --git a/UPGRADE.md b/UPGRADE.md index d540ceb..dbd26d7 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,4 +1,4 @@ -Upgrading from v1.x to v2.x +# Upgrading from v1.x to v2.x ## Removed @@ -57,8 +57,6 @@ For `ThrottlesRequest`, migrate to [Laravel Rate Limiting](https://github.com/la * Now the trait will automatically re-route all Eloquent builder macros to the public static methods in the Scope. There is no longer need to return closures or what not. -> This can have up to 100% more performance and makes the code more readable. - ### ModelType changed * The `getQualifiedTypeColumn` method has been changed to `getModelTypeColumn`. From 2bae4b907503f0ac53dd942b6ef9fff991a4ffba Mon Sep 17 00:00:00 2001 From: DarkGhostHunter Date: Mon, 25 May 2020 02:11:10 -0400 Subject: [PATCH 6/6] Fixed malformed composer.json. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f99a798..bb34d94 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "guzzlehttp/guzzle": "^6.5" }, "require-dev": { - "orchestra/testbench": "^5.2", + "orchestra/testbench": "^5.2" }, "autoload": { "psr-4": {