qossmic / deptrac

Keep your architecture clean.
https://qossmic.github.io/deptrac
MIT License
2.59k stars 136 forks source link

Modularization and Encapsulation Support in Deptrac Configuration #1404

Closed plumthedev closed 4 months ago

plumthedev commented 4 months ago

Some architectural approaches provide suggestions on how files should be organized within a project. One such approach is building modules based on the Package by Feature principle.

Package by Feature suggests organizing modules based on the functionalities they provide, thereby increasing cohesion and reducing dependencies. Following this approach, we can create the following directory structure:

src:
    - User
        - Application
            - Service
        - Domain
            - Dto
        - Infrastructure
            - Http
            - Service
    - Role
        - Application
            - Service
        - Domain
            - Dto
        - Infrastructure
            - Http
            - Service
    - Order
        - Application
            - Service
        - Domain
            - Dto
        - Infrastructure
            - Http
            - Service

This structure divides the code into modules: User, Role, and Order. Each module is then divided into specific layers: Application, Domain, and Infrastructure. These layers further break down into specific functionalities such as Service, Dto, and Http.

This structure aligns with the Package by Feature approach and hexagonal architecture principles. It enforces certain rules:

Other modules are aware of the existence of ports and domains of other components but are agnostic to specific implementations. Only the input and output matter for executing an action. This approach helps build modules with high cohesion and low coupling, practically independent of implementations.

However, there is a missing capability in Deptrac to define module names while considering implementation privacy. While the private: true option exists, its behavior doesn't fully meet expectations.

For instance, if I want to specify that Order/Infrastructure/Http can only depend on:

I would have to create six independent layers and specify them in the ruleset for Order/Infrastructure/Http. This becomes cumbersome, especially considering there could be dozens of such modules, each with several different functionalities (Repository, Service, Mapper, Factory, etc.).

My proposal is to add support for checking code modularization and encapsulating these modules within Deptrac. For example:

layers:
  # Application
  - name: Application Service
    public: true
    collectors:
      - type: className
        value: App\\Component\\(?<module>[a-zA-Z]+)\\Application\\Service\\.*
  # Domain
  - name: Domain Dto
    public: true
    collectors:
      - type: className
        value: App\\Component\\(?<module>[a-zA-Z]+)\\Domain\\Dto\\.*
  # Infrastructure
  - name: Infrastructure Http
    collectors:
      - type: className
        value: App\\Component\\(?<module>[a-zA-Z]+)\\Infrastructure\\Http\\.*
  - name: Infrastructure Service
    collectors:
      - type: className
        value: App\\Component\\(?<module>[a-zA-Z]+)\\Infrastructure\\Service\\.*
ruleset:
  Domain Dto:
  Application Service:
    - Domain Dto
  Infrastructure Http:
    - Infrastructure Http
    - Infrastructure Service
  Infrastructure Service:
    - Application Service
    - Domain Dto

The introduced changes include:

With this configuration:

Designating a layer as a module is achieved by adding a "Named capturing group." The public: true option for unmodularized layers would not affect any other tool behavior. Thus, backward compatibility is preserved.

patrickkusebauch commented 4 months ago

Hi, I see what you are trying to do here, as I personally also structure my projects into modules as you are describing here.

I can share my experience, and how I solve this problem with the current capabilities of deptrac. To me, what are you trying to achieve is too complex for a mark-up language like YAML. Luckily, deptrac also provides a way to specify configuration via PHP. And I think for your use case, it is a natural fit.

    $finder = \Nette\Utils\Finder::findDirectories('*')->from('src'); //to get all your modules
    foreach($finder as $module) {
        //specify layers that are present for each module
        //specify rulesets that are present between modules and within modules
        //(optionally) specify formatter groups for Graphviz to have nicer graphs
    }

I take it a step further and have a deptrac.php file for each module like this:

//top project level config
    $finder = \Nette\Utils\Finder::findFiles('*/deptrac.php')->from('src');

    foreach ($finder as $file) {
        require_once $file->getPathname();
        $namespace = str_replace('src/', '\\My\\Namespace\\', $file->getPath());
        $layers = $namespace . '\\layers';
        $config->layers(...$layers());
        $rulesets = $namespace . '\\rulesets';
        $config->rulesets(...$rulesets());
        $graphvizFormat = $namespace . '\\graphvizFormat';
        $graphvizFormat($formatter);
    }

// deptrac.php in each module
/**
 * @return list<Layer>
 */
function layers(): array
{
    return [
        Layer::withName('Branding')
            ->collectors(
                DirectoryConfig::create('src/Branding/.*'),
                ClassLikeConfig::create('^Nette\\Routing\\.*'),
                ComposerConfig::create()
                    ->addPackage('contributte/menu-control')
                    ->addPackage('nette/routing')
            ),
    ];
}

/**
 * @return list<Ruleset>
 */
function rulesets(): array
{
    return [
        Ruleset::forLayer(Layer::withName('Branding'))
            ->accesses(
                Layer::withName('Generic Domain'),
                Layer::withName('Nette'),
                Layer::withName('Logging'),
                //and more
            ),
    ];
}

function graphvizFormat(GraphvizConfig $graphvizConfig): void
{
    $graphvizConfig->hiddenLayers(Layer::withName('Branding'));
}
plumthedev commented 4 months ago

Looks great, however I see that $config and $formatter are not defined at top level config.

CLI

sail@f7adfa5bd9b5:/var/www/html$ ./vendor/bin/deptrac --config-file=deptrac.php 
PHP Warning:  Undefined variable $config in /var/www/html/deptrac.php on line 21
PHP Warning:  Undefined variable $formatter in /var/www/html/deptrac.php on line 2

/var/www/html/deptrac.php

<?php

declare(strict_types = 1);

$finder = \Nette\Utils\Finder::findFiles('*/deptrac.php')->from('app');

foreach ($finder as $file) {
    require_once $file->getPathname();
    $namespace = str_replace('src/', '\\My\\Namespace\\', $file->getPath());

    $layers = $namespace . '\\layers';
    $config->layers(...$layers());

    $rulesets = $namespace . '\\rulesets';
    $config->rulesets(...$rulesets());

    $graphvizFormat = $namespace . '\\graphvizFormat';
    $graphvizFormat($formatter);
}

dd($config, $formatter);

Am I doing something wrong? Do you have any examples in docs?

patrickkusebauch commented 4 months ago

You can reference https://github.com/qossmic/deptrac/blob/2.0.x/docs/blog/2023-05-11_PHP_configuration.md or https://github.com/qossmic/deptrac-src/blob/2.0.x/deptrac.config.php.

From there, you should be able to get the $config. For formatter - $formatter = GraphvizConfig::create().

plumthedev commented 4 months ago

Thanks @patrickkusebauch It looks definitely like something what fulfill my needs and will be enough here. I will present my solution when it will be built and ready, for now we can close this thread!