php – Any disadvantages/problems im missing or improvements on this new Laravel base Model and FormRequest


Introduction

I’ve improved, IMO, Laravel’s base Model and FormRequest. It’s working as expected. But, before actually implementing this any further in my application, i want to have my classes checked on any problems i did’nt oversee. Also any suggestions for improvements are welcome. And feel free to use it in your application.


FormRequest

Improvements

  • Easy to use attribute manipulation before validation. Just overwrite the beforeValidation() in your ModelRequest
  • Policy setup is required by default. If no policy is defined, an error will be thrown. This is done because it’s often forgot to implement while this is beïng a serious security risk.
  • Rules are now defined on the model.
  • If authorization and validation are required for the request, is defined on the model.

Code

<?php

namespace AppAppFoundation;

use IlluminateFoundationHttpFormRequest as BaseFormRequest;

class FormRequest extends BaseFormRequest
{
    /**
     * The attributes that should be modified before passing it to the validator.
     *
     * @var array
     */
    public function beforeValidation()
    {
        return (
            //
        );
    }

    /**
     * Modify the attributes as defined in beforeValidation() before validation
     *
     * @return IlluminateContractsValidationValidator
     */
    protected function getValidatorInstance()
    {
        $this->merge($this->beforeValidation());
        
        return parent::getValidatorInstance();
    }

    /**
     * The rules that should apply on the request which are defined in the model
     * 
     * @return array
     */
    public function rules()
    {
        return $this->getModel()->rules();
    }

    /**
     * Determine if the request is authorized.
     * Authorization should be handled in policies.
     *
     * @return bool
     */
    public function authorize()
    {
        if ($this->getModel()->authorization && is_null(policy(get_class($this->getModel())))) {
            throw new Exception('There are no policies set for '.get_class($this->getModel()).'. You should set the policies in AuthServiceProvider.php');
        }

        return true;
    }
}

Usage:

<?php

namespace AppHttp{namespace};

use AppAppFoundationFormRequest;
use AppDomain{namespace};

class {Model}Request extends FormRequest
{
    /**
     * Return a new model instance
     *
     * @return Model
     */
    public function getModel()
    {
        return new {Model};
    }
}

Model

Improvements

  • Defining if validation and authorization are required for the corresponding model. Defaults to true. Setting it to false is usually only done for debugging purposes. You can overwrite the $validation and $authorization properties on your model.
  • If no validation rules are set and the $validation property is not set to false, an error will be thrown when trying to create/update the corresponding model.
  • Validation rules can be set per request type (post, put/patch, delete). If validation needs to apply on both store and update requests, just overwrite the validation() method in your model. Else overwrite the defaultValidation(), storeValidation(), updateValidation() and deleteValidation() methods.
  • Mass assignment protection disabled by default. Only id property is guarded.
  • All date and datetime columns will be automaticly casted to a Carbon instance. No need to set the $dates or $casts property. If you need additional casts, overwrite the casts() method (instead of property).
  • The default connection will be set automaticly. When using relations on multiple different connections, laravel does’nt know which connection to use by default and needs to be explicitly defined on the model.

Code

<?php

namespace AppAppFoundation;

use IlluminateDatabaseEloquentModel as BaseModel;

class Model extends BaseModel
{
    /**
     * Construct the Model instance and setup the properties
     * 
     * @return void
     */
    public function __construct()
    {
        $this->casts = array_merge(
            $this->getCastableDateColumns(), $this->casts()
        );
        $this->connection = config('database.default');
    }

    /**
     * The attributes that are not mass assignable.
     *
     * @var array
     */
    protected $guarded = (
        'id',
    );

    /**
     * Determine if validation is required.
     *
     * @var bool
     */
    public $validation = true;

    /**
     * Determine if authorization is required.
     *
     * @var bool
     */
    public $authorization = true;
    
    /**
     * The attributes that should be casted to a given data type.
     *
     * @return array
     */
    public function casts()
    {
        return ();
    }

    /**
     * The rules that should apply on the request.
     * This method is staticly beïng called from within the FormRequest
     * 
     * @return array
     */
    public static function rules() 
    {
        $self = new static;

        if ($self->validation && empty($self->validation())) {
            throw new Exception('There are no validation rules set for '.get_class($self).'. Validation can be disabled by overwriting the $validation property.');
        }

        return $self->validation();
    }

    /**
     * The validation rules that should apply on the request
     * If no rule seperation (store/update) is required, the validation method 
     * can be overwritten so that it returns the validation rules.
     * 
     * @return array
     */
    public function validation()
    {
        $request = request();

        if ($request->method() == 'POST') 
        {
            return array_merge(
                $this->defaultValidation(), 
                $this->storeValidation()
            );
        } 
        elseif ($request->method() == 'PUT' || $request->method() == 'PATCH') 
        {
            return array_merge(
                $this->defaultValidation(), 
                $this->updateValidation()
            );
        } 
        elseif ($request->method() == 'DELETE') 
        {
            return $this->deleteValidation();
        }
    }

    /**
     * The validation rules that should apply on both POST and PUT/PATCH requests
     * Can be used when separating validation logic based on the request type (POST, PUT/PATCH or DELETE)
     * If no seperation is required, the validation() method can be overwritten.
     *
     * @return array
     */
    public function defaultValidation()
    {
        return ();
    }

    /**
     * The validation rules that should only apply on store (POST) requests
     *
     * @return array
     */
    public function storeValidation()
    {
        return ();
    }

    /**
     * The validation rules that should only apply on update (PUT/PATCH) requests
     *
     * @return array
     */
    public function updateValidation()
    {
        return ();
    }

    /**
     * The validation rules that should only apply on delete (DELETE) requests
     *
     * @return array
     */
    public function deleteValidation()
    {
        return ();
    }

    public function getCastableDateColumns()
    {
        $tableColumns = DB::getDoctrineSchemaManager()->listTableColumns($this->getTable());
        
        $castableDateTypes = (
            'DoctrineDBALTypesDateTimeType' => 'datetime',
            'DoctrineDBALTypesDateType' => 'date',
        );

        $castableDateColumns = ();

        foreach ($tableColumns as $column) {
            if (in_array(get_class($column->getType()), array_keys($castableDateTypes))) {
                $castableDateColumns($column->getName()) = $castableDateTypes(get_class($column->getType()));
            }
        }

        return $castableDateColumns;
    }
}

Usage:

<?php

namespace AppDomain{namespace};

use AppAppFoundationModel;

class {Model} extends Model
{
    public function validation()
    {
        return (
            //
        );
    }
}
```