DEV Community

david duymelinck
david duymelinck

Posted on

Laravel: creating an artisan DDD make command (fase 1)

Introduction

In my post about a modular file structure in Laravel I mentioned that the Laravel modules package had artisan commands.

This weekend it got me thinking about DDD file structures. If you look at the current file structure of Laravel and how DDD is structured, most of the directories belong in the infrastructure and interface categories and only a few in the application category.

src/
├── Application/
│ ├── Services/
│ ├── Commands/
│ ├── Queries/
│ └── DTOs/
├── Domain/
│ ├── Entities/
│ ├── ValueObjects/
│ ├── Aggregates/
│ ├── Services/
│ ├── Repositories/
│ └── Events/
├── Infrastructure/
│ ├── Repositories/
│ ├── ORM/
│ ├── Services/
│ └── EventListeners/
├── Interface/
│ ├── Controllers/
│ ├── Views/
│ └── Middlewares/
└── SharedKernel/
└── Utils/

And this is just one of the DDD file structures, there are alternatives and they all have their pros and cons.
Like my modular file structure example there are DDD file structures that also can have directories that have no hard coded names.

This means an artisan command that caters to create files in a chosen DDD file structure must be configurable.

Starting to create the command

So the first thing I thought of was a config file that matched the current artisan commands. And group the commands per DDD file structure type.

return [

'structure' => 'ddd-layered'

'structure_choices' => [
   'ddd-layered' => [
       'controller_namespace' => 'App\Interface\Controllers',
       'model_namespace' => 'App\Infrastructure\Models',
   ]
]
];
Enter fullscreen mode Exit fullscreen mode

The problem was obvious from the start that there was a big gap, because the current artisan make commands have no way to create files in the rest of the directories.

I cleaned the board and the next idea was to add the file structure to the structure_choices group.

return [

'structure' => 'ddd-layered'

'structure_choices' => [
   'ddd-layered' => [
       'App' => [
           'Infrastructure' => [
               'Models'
           ],
           'Interface' => [
              'Controllers'
           ]  
       ],
   ]
]
];
Enter fullscreen mode Exit fullscreen mode

I'm not a big fan of typing a lot when I execute commands. So I thought what if it is possible to use the first letter of each directory to get to the directory I want to add a file.

This idea also implies that there is only one command, instead of multiple commands.

class ConfigurableMakeCommand extends Command
{
    protected $signature = 'make:configurable {shortpath : First letter of each path directory}';

    public function handle(): bool
    {
        $shortpath = $this->argument('shortpath');
        $directoryStructure = config('directories.structure');
        $directoryStructureArray = config('directories.structure_choices.'.$directoryStructure);
        $foundPaths = $this->findPaths($directoryStructureArray, $shortpath);

        if(empty($foundPaths)) {
            $this->components->error('No paths found for '.$shortpath);

            return false;
        }

        if(count($foundPaths) == 1) {
            $foundPath = join('\\', $foundPaths[0]['dirs']);
            $confirm = confirm("Would you want to add a file to $foundPath?");

            if(!$confirm) {
                $this->components->error('Sorry you had to cancel the file creation. Please try again.');

                return false;
            }
        } elseif(count($foundPaths) > 1) {
            $foundPathStrings = array_map(function ($foundPath) {
                return join('\\', $foundPath['dirs']);
            }, $foundPaths);

            $choice = select('What is the path you looked for?', $foundPathStrings);

            if($choice) {
                $foundPath = $choice;
            }
        }
    }

    private function findPaths(array $pathStructure, string $firstletters,  array $found = []) : array {
        $firstletters = strtolower($firstletters);

        if(strlen($firstletters) == 1) {
            foreach($found as $i => $pair) {
                if(is_string($pair['value']) && str_starts_with(strtolower($pair['value']), $firstletterKeys)) {
                    $found[$i]['dirs'][] = $pair['value'];
                    $found[$i]['value'] = '';
                }else {
                    unset($found[$i]);
                }
            }

            return $found;
        }

        $firstLetter = substr($firstletters, 0, 1);

        if(empty($found)) {
            foreach ($pathStructure as $key => $value) {
                if (str_starts_with(strtolower($key), $firstLetter) || str_starts_with($key, '{')) {
                    $found[] = [
                        'dirs' => [$key],
                        'value' => $value,
                    ];

                }
            }
        } else {
            foreach($found as $i => $pair) {
                if(is_array($pair['value'])) {
                    foreach($pair['value'] as $key => $value) {
                        if(str_starts_with(strtolower($key),  $firstLetter) || str_starts_with($key, '{')) {
                            $found[$i]['dirs'][] = $key;
                            $found[$i]['value'] = $value;
                        }
                    }
                }
            }
        }

        return $this->collectArrayKeys($pathStructure, substr($firstletters, 1), $found);
    }
}
Enter fullscreen mode Exit fullscreen mode

The star of the show at this moment is the findPaths method.
For the people who are not familiar with recursive functions I will explain the different parts.

$firstletters = strtolower($firstletters);

        if(strlen($firstletters) == 1) {
            foreach($found as $i => $pair) {
                if(is_string($pair['value']) && str_starts_with(strtolower($pair['value']), $firstletterKeys)) {
                    $found[$i]['dirs'][] = $pair['value'];
                    $found[$i]['value'] = '';
                }else {
                    unset($found[$i]);
                }
            }

            return $found;
        }
Enter fullscreen mode Exit fullscreen mode

This stops the function from calling itself again, because otherwise it will create an endless loop.
And it also returns an array of directories in the dirs key.
I return the whole $found array because it is possible there are multiple paths.

$firstLetter = substr($firstletters, 0, 1);

        if(empty($found)) {
            foreach ($pathStructure as $key => $value) {
                if (str_starts_with(strtolower($key), $firstLetter) || str_starts_with($key, '{')) {
                    $found[] = [
                        'dirs' => [$key],
                        'value' => $value,
                    ];

                }
            }
        } else {
            foreach($found as $i => $pair) {
                if(is_array($pair['value'])) {
                    foreach($pair['value'] as $key => $value) {
                        if(str_starts_with(strtolower($key),  $firstLetter) || str_starts_with($key, '{')) {
                            $found[$i]['dirs'][] = $key;
                            $found[$i]['value'] = $value;
                        }
                    }
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

This part can be seen as the body of the method. Here is where the logic is.
As you can see I only use the array from the config file the first time, all other times the method is executed the $found array is used. This is a common way to work with recursion. Another example is the PHP array_reduce function.

I already added a check for user defined directories with str_starts_with($key, '{').
The idea is to create a prompt in the handle method when a path has a {name} pattern. So that the developer can add the directory name.

return $this->collectArrayKeys($pathStructure, substr($firstletters, 1), $found);
Enter fullscreen mode Exit fullscreen mode

This is the recursive call. As you can see I remove the first letter from the string as a sign that the function has to search for the the first letter one level deeper in the structure array.

Observant people might already understand that this method only works for end paths. For this fase of the development it is good enough. In a later fase there should be a way to get to all the paths in the structure.

In the handle method I already added the logic for the following scenarios:

  • no paths found
  • one path found
  • multiple paths found

For the first one the command execution stops. In the other cases the code will continue with one path.

Next steps

This is a good setup to create a file. But creating a file with almost no content is not faster than doing the same in the command interface or GUI.
So I have to think how the command can make adding content to the file faster or more comfortable or both.

An other alternative I'm considering is to use the directory structure as a way to create the structure in the file system and use the file system to loop over.

In my dreams this would be the ideal setup, instead of the standard file structure that most frameworks have. Install the framework and have an after installation prompt to select the file structure.

Top comments (2)

Collapse
 
snipertomcat profile image
Jesse Griffin

Awesome tutorial and solid code examples—they do a great job of showing how DDD works in the real world!

Collapse
 
xwero profile image
david duymelinck

Thank you.

While the idea started because of the variations in DDD file structures.
I think the implementation goes beyond DDD. It is possible to add any file structure, for example the Laravel or the Symfony file structures.

After a night sleep I woke up with the thought this command could be a framework independent. Why limit this to one framework.