Giter Site home page Giter Site logo

lunarstorm / laravel-ddd Goto Github PK

View Code? Open in Web Editor NEW
52.0 4.0 5.0 233 KB

Toolkit for domain driven design (DDD) in Laravel. Provides artisan commands to generate domain models, DTOs, view models, value objects, and much more.

License: MIT License

PHP 100.00%
artisan ddd domain-driven-design laravel

laravel-ddd's Introduction

Domain Driven Design toolkit for Laravel

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

Laravel-DDD is a toolkit to support domain driven design (DDD) in Laravel applications. One of the pain points when adopting DDD is the inability to use Laravel's native make commands to generate domain objects since they are typically stored outside the App\* namespace. This package aims to fill the gaps by providing equivalent commands such as ddd:model, ddd:dto, ddd:view-model and many more.

Installation

You can install the package via composer:

composer require lunarstorm/laravel-ddd

You may initialize the package using the ddd:install artisan command. This will publish the config file, register the domain path in your project's composer.json psr-4 autoload configuration on your behalf, and allow you to publish generator stubs for customization if needed.

php artisan ddd:install

Deployment

In production, run ddd:cache during the deployment process to optimize autoloading.

php artisan ddd:cache

Version Compatibility

Laravel LaravelDDD
9.x - 10.24.x 0.x 0.x README
10.25.x 1.x
11.x 1.x

See UPGRADING for more details about upgrading from 0.x.

Usage

Syntax

All domain generator commands use the following syntax:

# Specifying the domain as an option
php artisan ddd:{object} {name} --domain={domain}

# Specifying the domain as part of the name (short-hand syntax)
php artisan ddd:{object} {domain}:{name}

# Not specifying the domain at all, which will then 
# prompt for it (with auto-completion)
php artisan ddd:{object} {name}

Available Commands

Generators

The following generators are currently available, shown using short-hand syntax:

# Generate a domain model
php artisan ddd:model Invoicing:Invoice

# Generate a domain model with factory
php artisan ddd:model Invoicing:Invoice -f
php artisan ddd:model Invoicing:Invoice --factory

# Generate a domain factory
php artisan ddd:factory Invoicing:InvoiceFactory
php artisan ddd:factory Invoicing:InvoiceFactory --model=Invoice # optionally specifying the model

# Generate a data transfer object
php artisan ddd:dto Invoicing:LineItemPayload

# Generates a value object
php artisan ddd:value Shared:DollarAmount

# Generates a view model
php artisan ddd:view-model Invoicing:ShowInvoiceViewModel

# Generates an action
php artisan ddd:action Invoicing:SendInvoiceToCustomer

# Extended Commands 
# These extend Laravel's respective make:* commands and places the objects into the domain layer
php artisan ddd:cast Invoicing:MoneyCast
php artisan ddd:channel Invoicing:InvoiceChannel
php artisan ddd:command Invoicing:InvoiceDeliver
php artisan ddd:event Invoicing:PaymentWasReceived
php artisan ddd:exception Invoicing:InvoiceNotFoundException
php artisan ddd:job Invoicing:GenerateInvoicePdf
php artisan ddd:listener Invoicing:HandlePaymentReceived
php artisan ddd:mail Invoicing:OverduePaymentReminderEmail
php artisan ddd:notification Invoicing:YourPaymentWasReceived
php artisan ddd:observer Invoicing:InvoiceObserver
php artisan ddd:policy Invoicing:InvoicePolicy
php artisan ddd:provider Invoicing:InvoiceServiceProvider
php artisan ddd:resource Invoicing:InvoiceResource
php artisan ddd:rule Invoicing:ValidPaymentMethod
php artisan ddd:scope Invoicing:ArchivedInvoicesScope

# Laravel 11+ only
php artisan ddd:class Invoicing:Support/InvoiceBuilder
php artisan ddd:enum Customer:CustomerType
php artisan ddd:interface Customer:Contracts/Invoiceable
php artisan ddd:trait Customer:Concerns/HasInvoices

Generated objects will be placed in the appropriate domain namespace as specified by ddd.namespaces.* in the config file.

Other Commands

# Show a summary of current domains in the domain folder
php artisan ddd:list

# Cache domain manifests (used for autoloading)
php artisan ddd:cache

# Clear the domain cache
php artisan ddd:clear

Advanced Usage

Nested Objects

For any ddd:* generator command, nested objects can be specified with forward slashes.

php artisan ddd:model Invoicing:Payment/Transaction
# -> Domain\Invoicing\Models\Payment\Transaction

php artisan ddd:action Invoicing:Payment/ProcessTransaction
# -> Domain\Invoicing\Actions\Payment\ProcessTransaction

php artisan ddd:exception Invoicing:Payment/PaymentFailedException
# -> Domain\Invoicing\Exceptions\Payment\PaymentFailedException

This is essential for objects without a fixed namespace such as class, interface, trait, each of which have a blank namespace by default. In other words, these objects originate from the root of the domain.

php artisan ddd:class Invoicing:Support/InvoiceBuilder
# -> Domain\Invoicing\Support\InvoiceBuilder

php artisan ddd:interface Invoicing:Contracts/PayableByCreditCard
# -> Domain\Invoicing\Contracts\PayableByCreditCard

php artisan ddd:interface Invoicing:Models/Concerns/HasLineItems
# -> Domain\Invoicing\Models\Concerns\HasLineItems

Overriding Configured Namespaces at Runtime

If for some reason you need to generate a domain object under a namespace different to what is configured in ddd.namespaces.*, you may do so using an absolute name starting with /. This will generate the object from the root of the domain.

# The usual: generate a provider in the configured provider namespace
php artisan ddd:provider Invoicing:InvoiceServiceProvider 
# -> Domain\Invoicing\Providers\InvoiceServiceProvider

# Override the configured namespace at runtime
php artisan ddd:provider Invoicing:/InvoiceServiceProvider
# -> Domain\Invoicing\InvoiceServiceProvider

# Generate an event inside the Models namespace (hypothetical)
php artisan ddd:event Invoicing:/Models/EventDoesNotBelongHere
# -> Domain\Invoicing\Models\EventDoesNotBelongHere

# Deep nesting is supported
php artisan ddd:exception Invoicing:/Models/Exceptions/InvoiceNotFoundException
# -> Domain\Invoicing\Models\Exceptions\InvoiceNotFoundException

Subdomains (nested domains)

Subdomains can be specified with dot notation wherever a domain option is accepted.

# Domain/Reporting/Internal/ViewModels/MonthlyInvoicesReportViewModel
php artisan ddd:view-model Reporting.Internal:MonthlyInvoicesReportViewModel

# Domain/Reporting/Customer/ViewModels/MonthlyInvoicesReportViewModel
php artisan ddd:view-model Reporting.Customer:MonthlyInvoicesReportViewModel

# (supported by all commands where a domain option is accepted)

Customization

This package ships with opinionated (but sensible) configuration defaults. You may customize by publishing the config file and generator stubs as needed:

php artisan vendor:publish --tag="ddd-config"
php artisan vendor:publish --tag="ddd-stubs"

Note that the extended commands do not publish ddd-specific stubs, and inherit the respective application-level stubs published by Laravel.

Domain Autoloading and Discovery

Autoloading behaviour can be configured with the ddd.autoload configuration option. By default, domain providers, commands, policies, and factories are auto-discovered and registered.

'autoload' => [
    'providers' => true,
    'commands' => true,
    'policies' => true,
    'factories' => true,
],

Service Providers

When ddd.autoload.providers is enabled, any class within the domain layer extending Illuminate\Support\ServiceProvider will be auto-registered as a service provider.

Console Commands

When ddd.autoload.commands is enabled, any class within the domain layer extending Illuminate\Console\Command will be auto-registered as a command when running in console.

Policies

When ddd.autoload.policies is enabled, the package will register a custom policy discovery callback to resolve policy names for domain models, and fallback to Laravel's default for all other cases. If your application implements its own policy discovery using Gate::guessPolicyNamesUsing(), you should set ddd.autoload.policies to false to ensure it is not overridden.

Factories

When ddd.autoload.factories is enabled, the package will register a custom factory discovery callback to resolve factory names for domain models, and fallback to Laravel's default for all other cases. Note that this does not affect domain models using the Lunarstorm\LaravelDDD\Factories\HasDomainFactory trait. Where this is useful is with regular models in the domain layer that use the standard Illuminate\Database\Eloquent\Factories\HasFactory trait.

If your application implements its own factory discovery using Factory::guessFactoryNamesUsing(), you should set ddd.autoload.factories to false to ensure it is not overridden.

Ignoring Paths During Autoloading

To specify folders or paths that should be skipped during autoloading discovery, add them to the ddd.autoload_ignore configuration option. By default, the Tests and Migrations folders are ignored.

'autoload_ignore' => [
    'Tests',
    'Database/Migrations',
],

Paths specified here are relative to the root of each domain. e.g., src/Domain/Invoicing/{path-to-ignore}. If more advanced filtering is needed, a callback can be registered using DDD::filterAutoloadPathsUsing(callback $filter) in your AppServiceProvider's boot method:

use Lunarstorm\LaravelDDD\Facades\DDD;
use Symfony\Component\Finder\SplFileInfo;

DDD::filterAutoloadPathsUsing(function (SplFileInfo $file) {
    if (basename($file->getRelativePathname()) === 'functions.php') {
        return false;
    }
});

The filter callback is based on Symfony's Finder Component.

Disabling Autoloading

You may disable autoloading by setting the respective autoload options to false in the configuration file as needed, or by commenting out the autoload configuration entirely.

// 'autoload' => [
//     'providers' => true,
//     'commands' => true,
//     'policies' => true,
//     'factories' => true,
// ],

Autoloading in Production

In production, you should cache the autoload manifests using the ddd:cache command as part of your application's deployment process. This will speed up the auto-discovery and registration of domain providers and commands. The ddd:clear command may be used to clear the cache if needed.

Configuration File

This is the content of the published config file (ddd.php):

return [

    /*
    |--------------------------------------------------------------------------
    | Domain Path
    |--------------------------------------------------------------------------
    |
    | The path to the domain folder relative to the application root.
    |
    */
    'domain_path' => 'src/Domain',

    /*
    |--------------------------------------------------------------------------
    | Domain Namespace
    |--------------------------------------------------------------------------
    |
    | The root domain namespace.
    |
    */
    'domain_namespace' => 'Domain',

    /*
    |--------------------------------------------------------------------------
    | Domain Object Namespaces
    |--------------------------------------------------------------------------
    |
    | This value contains the default namespaces of generated domain
    | objects relative to the domain namespace of which the object
    | belongs to.
    |
    | e.g., Domain\Invoicing\Models\*
    |       Domain\Invoicing\Data\*
    |       Domain\Invoicing\ViewModels\*
    |       Domain\Invoicing\ValueObjects\*
    |       Domain\Invoicing\Actions\*
    |
    */
    'namespaces' => [
        'model' => 'Models',
        'data_transfer_object' => 'Data',
        'view_model' => 'ViewModels',
        'value_object' => 'ValueObjects',
        'action' => 'Actions',
        'cast' => 'Casts',
        'class' => '',
        'channel' => 'Channels',
        'command' => 'Commands',
        'enum' => 'Enums',
        'event' => 'Events',
        'exception' => 'Exceptions',
        'factory' => 'Database\Factories',
        'interface' => '',
        'job' => 'Jobs',
        'listener' => 'Listeners',
        'mail' => 'Mail',
        'notification' => 'Notifications',
        'observer' => 'Observers',
        'policy' => 'Policies',
        'provider' => 'Providers',
        'resource' => 'Resources',
        'rule' => 'Rules',
        'scope' => 'Scopes',
        'trait' => '',
    ],

    /*
    |--------------------------------------------------------------------------
    | Base Model
    |--------------------------------------------------------------------------
    |
    | The base class which generated domain models should extend. By default,
    | generated domain models will extend `Domain\Shared\Models\BaseModel`,
    | which will be created if it doesn't already exist.
    |
    */
    'base_model' => 'Domain\Shared\Models\BaseModel',

    /*
    |--------------------------------------------------------------------------
    | Base DTO
    |--------------------------------------------------------------------------
    |
    | The base class which generated data transfer objects should extend. By
    | default, generated DTOs will extend `Spatie\LaravelData\Data` from
    | Spatie's Laravel-data package, a highly recommended data object
    | package to work with.
    |
    */
    'base_dto' => 'Spatie\LaravelData\Data',

    /*
    |--------------------------------------------------------------------------
    | Base ViewModel
    |--------------------------------------------------------------------------
    |
    | The base class which generated view models should extend. By default,
    | generated domain models will extend `Domain\Shared\ViewModels\BaseViewModel`,
    | which will be created if it doesn't already exist.
    |
    */
    'base_view_model' => 'Domain\Shared\ViewModels\ViewModel',

    /*
    |--------------------------------------------------------------------------
    | Base Action
    |--------------------------------------------------------------------------
    |
    | The base class which generated action objects should extend. By default,
    | generated actions are based on the `lorisleiva/laravel-actions` package
    | and do not extend anything.
    |
    */
    'base_action' => null,

    /*
    |--------------------------------------------------------------------------
    | Autoloading
    |--------------------------------------------------------------------------
    |
    | Configure whether domain providers, commands, policies, and factories
    | should be auto-discovered and registered.
    |
    */
    'autoload' => [
        /**
         * When enabled, any class within the domain layer extending `Illuminate\Support\ServiceProvider`
         * will be auto-registered as a service provider
         */
        'providers' => true,

        /**
         * When enabled, any class within the domain layer extending `Illuminate\Console\Command`
         * will be auto-registered as a command when running in console.
         */
        'commands' => true,

        /**
         * When enabled, the package will register a custom policy discovery callback to resolve policy names
         * for domain models, and fallback to Laravel's default for all other cases.
         */
        'policies' => true,

        /**
         * When enabled, the package will register a custom factory discovery callback to resolve factory names
         * for domain models, and fallback to Laravel's default for all other cases.
         */
        'factories' => true,
    ],

    /*
    |--------------------------------------------------------------------------
    | Autoload Ignore Folders
    |--------------------------------------------------------------------------
    |
    | Folders that should be skipped during autoloading discovery,
    | relative to the root of each domain.
    |
    | e.g., src/Domain/Invoicing/<folder-to-ignore>
    |
    | If more advanced filtering is needed, a callback can be registered
    | using the `DDD::filterAutoloadPathsUsing(callback $filter)` in
    | the AppServiceProvider's boot method.
    |
    */
    'autoload_ignore' => [
        'Tests',
        'Database/Migrations',
    ],

    /*
    |--------------------------------------------------------------------------
    | Caching
    |--------------------------------------------------------------------------
    |
    | The folder where the domain cache files will be stored. Used for domain
    | autoloading.
    |
    */
    'cache_directory' => 'bootstrap/cache/ddd',
];

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

laravel-ddd's People

Contributors

dependabot[bot] avatar github-actions[bot] avatar jaspertey avatar meditto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

laravel-ddd's Issues

Implement support for model factories

It'd be nice to be able to do php artisan ddd:model -f and have it create the factory in the appropriate directory from the newFactory method in the BaseModel and have the protected $model attribute populated.

Originally posted by @funkymonk91 in #21

When configuring the domains path, namespaces are still using 'domains'

I updated my config to conform to my project file structure:

config/ddd.php

'paths' => [
        //
        // Path to the Domain layer.
        //
        'domains' => 'src/Domain',

I ran php artisan ddd:dto PurchaseOrder PurchaseOrderJobDetailsData and it put the file in the correct place (src/Domain/PurchaseOrder/Data).

The contents of the file are:

<?php

namespace Domains\PurchaseOrder\Data;

use Spatie\LaravelData\Data;

class PurchaseOrderJobDetailData extends Data
{
    public function __construct(
        // ...
    ) {
    }
}

Allow configuring domain namespace

Action items:

  • Introduce ddd.domain_path and ddd.domain_namespace config keys and deprecate the usage ddd.paths.

Discussed in #40

Originally posted by pelmered March 21, 2024
First of all, thank you so much for this package. I've been looking for something more lightweight land flexible like this for generating classes with support for DDD, but not do a lot more things and force me into opinionated ways to structure my code. This looks really great! There are just some generator commands that I am missing, like jobs, commands, enums, controllers or service providers, but I guess I could extend this package myself. Or do you have plans for adding some of this? I could send PRs for that.

Now for my current main issue.
I have my Domains at src/Domains but the namespace is just Domain\*. This is accomplished with this PSR-4 autoload directive: "Domain\\": "src/Domains/",
When I check in the code the namespace is just based on the configured domain path. I would like to be able to configure the namespace specifically.

I can make a PR for this if you want, but I'm not sure what the plan/purpose with the paths array in the config file. Why is this an array? To keep the support for setting multiple paths and have a reasonable structure of the config file, I think you have the break the backwards compatibility. For example like this in my case:

    'paths' => [
        'domains' => [
            // Base path to the Domain layer.
            'path' => 'src/Domains',
            // Base namespace for the domain layer.
            'namespace' => 'Domain',
        ]
    ],

This would also allow for more options here later without breaking backwards compatibility.

Let me know what you think.

Implement `HasDomainFactory` trait

Action item:

  • Refactor the current domain model factory implementation as a portable HasDomainFactory trait instead

Discussed in #30

Originally posted by meditto August 17, 2023
Hello, I hope you're all doing well. I've been using laravel-ddd for the past few days and have found it to be a valuable tool for my Laravel projects. I'd like to suggest a feature enhancement that I believe could further improve the package's usability.
The current process involves generating the BaseMdel. I believe this process could be simplified and made more user-friendly by introducing a new trait, HasDDDFactory.

The proposed HasDDDFactory trait would serve as a drop-in replacement for the existing HasFactory trait. It would automatically handle the detection of the appropriate factory path just like the BaseModel.

The flexibility of the HasDDDFactory trait could also extend to models that don't directly extend the Model object, such as the User model. This adaptability adds to its potential usefulness.

image

I would greatly appreciate your thoughts and insights on this proposal.

Autoloading issues

Hello again!

I'm having some issues with the autoloading. Primarily when deploying the code.

I like to put my related tests inside my domains (a command for generating tests would be great by the way), but that generates problems with the autoloading. The tests are only autoloaded when composer includes dev dependencies. But since theddd:cache command touches all files in the domain directory, this generates problems because the base test case is not autoloaded and thus I get this error: Class "Tests\TestCase" not found when running ddd:cache.

I created a repo where I demonstrate this problem here: https://github.com/pelmered/laravel-test-ddd
Just clone, run composer install --no-dev and then run ddd:cache to reproduce the problem.

I have also had problems with some other things, for example the migrations from babenkoivan/elastic-migrations. That package creates migrations with the same file format as the default Laravel migrations, but they also include a class name so they are not PSR autoload compliant. Here is how they look. When autoloading or running ddd:cache these files are included several times and therefore gives this kind of error: Cannot declare class CreateUsersIndex, because the name is already in use. I created a PR there to use anonymous classes, just like Laravels migrations, here.

As you can see, this is a general problem that probably needs to be addressed in this package in some way.
What do you think is the best way forward? Let me know if I can be of assistance.

Other than this, I'm happy to report that it worked very well in our application and that the transition was very smooth to start using the autoload provided in this package.

[1.x] Fix Base Models

This applies to the next branch only.

Observed an issue with ddd:view-model:

  • appeared to always try to create a base view model even though one exists
% php artisan ddd:view-model Shared:TestViewModel
Base view model Domain\Shared\ViewModels\ViewModel doesn't exist, generating...

   ERROR  Base View Model already exists.  

   INFO  View Model [src/Domain/Shared/ViewModels/TestViewModel.php] created successfully. 
  • view model stub incompatible with the refactored generator internals. Ends up inserting the import as
use Domain\\Shared\ViewModels\ViewModel;

with the extra slash. Should also create a failing test for this first before patching.

ddd:model does not respect custom base model

When customizing ddd.base_model in config, ddd:model thinks it doesn't exist and attempts to generate one for you. It ends up being a malformed class generated in the root domain folder.

Base ViewModel not accounted for

The ddd:make:view-model command is not yet complete; the handling of base ViewModel is not yet accounted for, and currently leads to a incomplete generated view model.

Support for Actions

I found myself wanting to generate domain Actions enough times that I think it's finally time to implement a corresponding ddd:action generator.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.