Skip to content

Commit

Permalink
refactor!: observer should imply implementation of validationRules() …
Browse files Browse the repository at this point in the history
…method

ValidateModel observer now requires observed models to implement the
SelfValidatingModel interface.

BREAKING CHANGE: models without validationRules() method will cause
fatal error.

Resolves #14
  • Loading branch information
geoffreyvanwyk committed Aug 8, 2024
1 parent 8bcf168 commit 1bfc5f2
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 77 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,23 @@ composer require spoorsny/laravel-model-validating-observer

## Usage

Add the `ObservedBy` attribute to your model, with
`ValidateModel::class` as its argument.
Add the `\Illuminate\Database\Eloquent\Attributes\ObservedBy` attribute to your model, with
`\Spoorsny\Laravel\Observers\ValidateModel::class` as its argument.

Add a public, static method to your model, named `validationRules()` that
returns an associative array with the validation rules and custom messages for
your model's attributes.
Implement the `\Spoornsy\Laravel\Contracts\SelfValidatingModel` interface
within your model by adding a public, static method to your model, named
`validationRules()` that returns an associative array with the validation rules
and custom messages for your model's attributes.

```php
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

use Spoorsny\Laravel\Contracts\SelfValidatingModel;
use Spoorsny\Laravel\Observers\ValidateModel;

#[ObservedBy(ValidateModel::class)]
class Car extends Model
class Car extends Model implements SelfValidatingModel
{
public static function validationRules(): array
{
Expand All @@ -52,7 +54,7 @@ class Car extends Model
```

The observer will check each instance of your model against the validation
rules during the `saving` event triggered by Eloquent. If validation fails, a
rules during the `saving` event triggered by Eloquent. If validation fails, an
`\Illuminate\Validation\ValidationException` will be thrown, preventing the
persistence of the invalid model instance.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,36 @@
// You should have received a copy of the GNU General Public License along with
// package spoorsny/laravel-model-validating-observer. If not, see <https://www.gnu.org/licenses/>.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
namespace Spoorsny\Laravel\Contracts;

/**
* Create database table used during testing.
* Specifies the methods that an \Illuminate\Database\Eloquent\Model subclass
* must implement in order to be validated by the
* \Spoorsny\Laravel\Observers\ValidateModel observer.
*
* @author Geoffrey Bernardo van Wyk <[email protected]>
* @copyright 2024 Geoffrey Bernardo van Wyk {@link https://geoffreyvanwyk.dev}
* @license {@link http://www.gnu.org/copyleft/gpl.html} GNU GPL v3 or later
*/
return new class () extends Migration {
interface SelfValidatingModel
{
/**
* Run the migrations.
* Rules for validating the model's attributes.
*
* An example of an array that must be return by this method:
*
* [
* 'rules' => [
* 'make' => 'required|string',
* 'model' => 'required|string',
* ],
* 'messages' => [
* 'make.required' => 'We need to know the make of the car.',
* ],
* ]
*
* @see {@link https://laravel.com/docs/11.x/validation#available-validation-rules}
* @return array<string,array<string,mixed>>
*/
public function up(): void
{
Schema::create('without_validation_rules', function (Blueprint $table) {
$table->id();
$table->string('make');
$table->string('model');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('without_validation_rules');
}
};
public static function validationRules(): array;
}
31 changes: 6 additions & 25 deletions src/Observers/ValidateModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,14 @@
namespace Spoorsny\Laravel\Observers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

use Spoorsny\Laravel\Contracts\SelfValidatingModel;

/**
* Validates any Eloquent model before persisting it.
*
* The Eloquent model's class must have:
* - the \Illuminate\Database\Eloquent\Attributes\ObservedBy attribute that
* points to this class.
* - a static method named validationRules that returns an associative array
* with keys 'rules' and 'messages' that each pairs with an array. The
* 'rules' array must match the $rules parameter of the Validator::make()
* method, and the 'messages' array must match the $messages parameter of
* the Validator::make() method.
* Validates any Eloquent model before persisting it by listening for the
* model's `saving` event.
*
* @see {@link https://laravel.com/docs/11.x/eloquent#observers}
* @see {@link https://laravel.com/docs/11.x/validation#manually-creating-validators}
Expand All @@ -42,12 +36,8 @@
*/
class ValidateModel
{
public function saving(Model $model): void
public function saving(Model & SelfValidatingModel $model): void
{
if (! $this->hasValidationRules($model)) {
return;
}

$validationRules = $model::validationRules();

$validator = Validator::make(
Expand All @@ -61,13 +51,4 @@ public function saving(Model $model): void
throw new ValidationException($validator, null, $validator->errors()) ;
}
}

private function hasValidationRules(Model $model): bool
{
$methods = (new \ReflectionClass(get_class($model)))->getMethods();

$methodNames = array_map(fn ($method) => $method->name, $methods);

return in_array(needle: 'validationRules', haystack: $methodNames);
}
}
3 changes: 2 additions & 1 deletion tests/Fixtures/Models/Car.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

use Spoorsny\Laravel\Contracts\SelfValidatingModel;
use Spoorsny\Laravel\Observers\ValidateModel;

/**
Expand All @@ -29,7 +30,7 @@
* @license {@link http://www.gnu.org/copyleft/gpl.html} GNU GPL v3 or later
*/
#[ObservedBy(ValidateModel::class)]
class Car extends Model
class Car extends Model implements SelfValidatingModel
{
use HasFactory;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,26 @@
* @license {@link http://www.gnu.org/copyleft/gpl.html} GNU GPL v3 or later
*/
#[ObservedBy(ValidateModel::class)]
class WithoutValidationRules extends Model
class NotSelfValidatingCar extends Model
{
use HasFactory;

/**
* Rules for validating the model's attributes.
*
* @see {@link https://laravel.com/docs/11.x/validation#available-validation-rules}
* @return array<string,array<string,mixed>>
*/
public static function validationRules(): array
{
return [
'rules' => [
'make' => 'required|string',
'model' => 'required|string',
],
'messages' => [
'make.required' => 'We need to know the make of the car.',
],
];
}
}
30 changes: 11 additions & 19 deletions tests/Unit/ValidateModelObserverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
namespace Spoorsny\Laravel\Tests\Unit;

use ReflectionClass;
use TypeError;

use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
Expand All @@ -27,7 +28,7 @@

use Spoorsny\Laravel\Observers\ValidateModel;
use Spoorsny\Laravel\Tests\Fixtures\Models\Car;
use Spoorsny\Laravel\Tests\Fixtures\Models\WithoutValidationRules;
use Spoorsny\Laravel\Tests\Fixtures\Models\NotSelfValidatingCar;
use Spoorsny\Laravel\Tests\TestCase;

/**
Expand All @@ -41,30 +42,21 @@ class ValidateModelObserverTest extends TestCase
use RefreshDatabase;

#[Test]
public function it_passes_a_new_instance_without_validation_rules(): void
public function it_requires_model_to_be_self_validating(): void
{
$this->assertObservedByMe(WithoutValidationRules::class);
$this->assertObservedByMe(NotSelfValidatingCar::class);

$car = new WithoutValidationRules();
$car->make = 'Volkswagen';
$car->model = 'Polo Vivo';
$car->save();
}
$this->expectException(TypeError::class);
$this->expectExceptionMessage(
'Spoorsny\Laravel\Observers\ValidateModel::saving(): '
. 'Argument #1 ($model) must be of type '
. 'Illuminate\Database\Eloquent\Model&Spoorsny\Laravel\Contracts\SelfValidatingModel'
);

#[Test]
public function it_passes_an_existing_instance_without_validation_rules(): void
{
$this->assertObservedByMe(WithoutValidationRules::class);

$car = new WithoutValidationRules();
$car = new NotSelfValidatingCar();
$car->make = 'Volkswagen';
$car->model = 'Polo Vivo';
$car->save();

$car = WithoutValidationRules::find(1);
$car->make = 'Mitsubishi';
$car->model = 'Triton';
$car->save();
}

#[Test]
Expand Down

0 comments on commit 1bfc5f2

Please sign in to comment.