Modular Laravel

Posted on



Introduction

The following Laravel project/directory structure represents a personal boilerplate modular/SOA structure that I use most of the time when starting a new Laravel project.

I found myself creating the same structure multiple times during the past couple of months so I decided to create a boilerplate project starter.



Core structure

The Core module contains the main interfaces, abstract classes and implementations



Directory overview

app
├── Modules
│   └── Core
│       ├── Controllers
│       |   ├── ApiController.php
|       |   └── Controller.php
│       ├── Exceptions
│       |   ├── FormRequestTableNotFoundException.php
│       |   ├── GeneralException.php
│       |   ├── GeneralIndexException.php
│       |   ├── GeneralSearchException.php
│       |   ├── GeneralStoreException.php
│       |   ├── GeneralNotFoundException.php
│       |   ├── GeneralDestroyException.php
|       |   └── GeneralUpdateException.php
│       ├── Filters
│       |   ├── QueryFilter.php
|       |   └── FilterBuilder.php
│       ├── Helpers
|       |   └── Helper.php
│       ├── Interfaces
│       |   ├── FilterInterface.php
│       |   ├── SearchInterface.php
|       |   └── RepositoryInterface.php
│       ├── Models
|       |   └── .gitkeep
│       ├── Repositories
|       |   └── Repository.php
│       ├── Requests
│       |   ├── FormRequest.php
│       |   ├── CreateFormRequest.php
│       |   ├── DeleteFormRequest.php
│       |   ├── SearchFormRequest.php
│       |   ├── UpdateFormRequest.php
|       |   └── ShowFormRequest.php
│       ├── Resources
│       |   └── .gitkeep 
│       ├── Scopes
|       |   └── .gitkeep
│       ├── Traits
│       |   ├── ApiResponses.php
|       |   └── Filterable.php
│       ├── Transformers
│       |   ├── EmptyResource.php
|       |   └── EmptyResourceCollection.php
│       └── 
└── 
Enter fullscreen mode

Exit fullscreen mode



Interfaces

The main interface is the RepositoryInterface which has the basic CRUD and some additional methods defined.


namespace AppModulesCoreInterfaces;

interface RepositoryInterface
{
    /**
     * @return mixed
     */
    public function findAll();

    /**
     * @param int $id
     * @return mixed
     */
    public function findById(int $id);

    /**
     * @param string $column
     * @param $value
     * @return mixed
     */
    public function findBy(string $column, $value);

    /**
     * @param array $data
     * @return mixed
     */
    public function create(array $data);

    /**
     * @param int $id
     * @param array $data
     * @return mixed
     */
    public function update(int $id, array $data);


    /**
     * @param int $id
     * @return mixed
     */
    public function delete(int $id);

}
Enter fullscreen mode

Exit fullscreen mode

The Repository class that implements the RepositoryInterface looks like this:


namespace AppModulesCoreRepositories;

use AppModulesCoreInterfacesRepositoryInterface;

class Repository implements RepositoryInterface
{
    /**
     * Model::class
     */
    public $model;

    /**
     * @return mixed
     */
    public function findAll()
    {
        return $this->model::all();
    }

    /**
     * @param int $id
     * @return mixed
     */
    public function findById(int $id)
    {
        return $this->model::find($id);
    }

    /**
     * @param string $column
     * @param $value
     * @return mixed
     */
    public function findBy(string $column, $value)
    {
        return $this->model::where($column, $value);
    }

    /**
     * @param array $data
     * @return mixed
     */
    public function create(array $data)
    {
        return $this->model::create($data)->fresh();
    }

    /**
     * @param int $id
     * @param array $data
     * @return mixed
     */
    public function update(int $id, array $data)
    {
        $item = $this->findById($id);
        $item->fill($data);
        $item->save();
        return $item->fresh();
    }

    /**
     * @param int $id
     * @return mixed|void
     */
    public function delete(int $id)
    {
        $this->model::destroy($id);
    }
}
Enter fullscreen mode

Exit fullscreen mode

The other two interfaces are SearchInterface and FilterInterface

The SearchInterface defines one method, this interface can be implemented by a specific Repository class per Module when there is a need for a Search filter while retrieving data from the database.


namespace AppModulesCoreInterfaces;

interface SearchInterface
{
    /**
     * @param array $request
     * @return mixed
     */
    public function search(array $request);
}
Enter fullscreen mode

Exit fullscreen mode

Example implementation of the SearchInterface

namespace AppModulesExampleRepositories;

class ExampleRepository extends Repository implements ExampleInterface, SearchInterface
{
    /**
     * @var string
     */
    public $model = Example::class;

    /**
     * @param array $request
     * @return mixed
     * @throws ExampleSearchException
     */
    public function search(array $request)
    {
        try {
            $query = $this->model::filterBy($request);

            $query->orderBy(Arr::get($request, 'order_by') ?? 'id', Arr::get($request, 'sort') ?? 'desc');

            return $query->paginate(Arr::get($request, 'per_page') ?? (new $this->model)->getPerPage());

        } catch (Exception $exception) {
            throw new ExampleSearchException($exception);
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

This can be further abstracted, but I will handle that in some future release

Also, the FilterInterface defines only one method and this interface is implemented per Filter class per module if there is a need for filtering by specific request key.


namespace AppModulesCoreInterfaces;

interface FilterInterface
{
    /**
     * @param $value
     * @return mixed
     */
    public function handle($value);
}
Enter fullscreen mode

Exit fullscreen mode

Example implementation of the FilterInterface


namespace AppModulesExampleFilters;

use AppModulesCoreFiltersQueryFilter;
use AppModulesCoreInterfacesFilterInterface;

class Name extends QueryFilter implements FilterInterface
{
    /**
     * @param $value
     * @return mixed|void
     */
    public function handle($value)
    {
        $this->query->where('name', 'like', '%' . $value . '%');
    }
}
Enter fullscreen mode

Exit fullscreen mode



Exceptions

The Exceptions directory contains the General exceptions that have some predefined $code and $message for the exception, this can be overridden when the custom exception per Module extends the General Exception.

As an example in the provided Module Example there are multiple exceptions defined



ExampleNotFoundException


namespace AppModulesExampleExceptions;

use AppModulesCoreExceptionsGeneralNotFoundException;

class ExampleNotFoundException extends GeneralNotFoundException
{

}

Enter fullscreen mode

Exit fullscreen mode

This extends the GeneralNotFoundException


namespace AppModulesCoreExceptions;

class GeneralNotFoundException extends GeneralException
{
    public $code = 404;

    /**
     * @return string|null
     */
    public function message(): ?string
    {
        return "The requested resource was not found in the database";
    }
}

Enter fullscreen mode

Exit fullscreen mode



Requests

The Requests directory contains the General Form Request abstract classes.

The main FormRequest class overrides the failedValidation method from src/Illuminate/Foundation/Http/FormRequest.php


abstract class FormRequest extends LaravelFormRequest
{
    /**
     * Handle a failed validation attempt.
     *
     * @param Validator $validator
     * @return void
     *
     */
    protected function failedValidation(Validator $validator)
    {
        $errors = (new ValidationException($validator))->errors();

        throw new HttpResponseException(
            response()->json(['errors' => $errors], Response::HTTP_UNPROCESSABLE_ENTITY)
        );
    }
}
Enter fullscreen mode

Exit fullscreen mode

Then each of the other abstract Form Request classes extends this abstract FormRequest



CreateFormRequest


namespace AppModulesCoreRequests;

abstract class CreateFormRequest extends FormRequest
{
    protected $table = '';

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    abstract public function rules();
}
Enter fullscreen mode

Exit fullscreen mode

Now when the abstract CreateFormRequest is extended in a Module, the class that extends this will have to implement the abstract method rules() where the validation rules are defined.



CreateExampleRequest


namespace AppModulesExampleRequests;

use AppModulesCoreRequestsCreateFormRequest;
use IlluminateValidationRule;

class CreateExampleRequest extends CreateFormRequest
{
protected $table = 'examples';

/**
* @inheritDoc
*/

public function rules(): array
{
return [
'name' => [
'required',
'string',
Rule::unique($this->table, 'name')
],
'example_type_id' => [
'required',
Rule::exists('example_types', 'id')
],
'is_active' => [

Leave a Reply

Your email address will not be published.