Tactical DDD, simplified for PHP. Tag your classes via PHP attributes and analyze forbidden relations.
βββββββββββββββββββββββββββββββββββ
β Project Source Code β
β (tagged PHP classes) β
ββββββββββββββββββ¬βββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββ
β Analyzer Layer β
β (PhpFileAnalyzer, AST) β
ββββββββββββββββββ¬βββββββββββββββββ
β
βββββββββ΄βββββββββ
βΌ βΌ
ββββββββββββββββ ββββββββββββββββββββ
β Relation β β Rule Engine β
β Extractor ββββ€ (Blacklist) β
β (Violations) β β (YAML/DI) β
ββββββββ¬ββββββββ ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Check API β
β β’ Check::className() β
β β’ Check::folder() β
β (throws on violations) β
ββββββββββββββ¬ββββββββββββββββββββββ
β
βββββββββΌβββββββββ
βΌ βΌ βΌ
βββββββββββ ββββββββ ββββββββββββ
β CLI β β HTML β βFramework β
β Command β βReportβ β Bundles β
βββββββββββ ββββββββ ββββββββββββ
Flow: Tagged source code β AST analysis β Rule validation β Check API β Output (CLI/Report/Framework)
composer require --dev makomweb/tactixRequirements:
- PHP ^8.2
- your source code being tagged with PHP Molecules
- install the tags package from PHP Molecules as a regular dependency via:
composer require xmolecules/phpmolecules- tag your classes with the available attributes:
use PHPMolecules\DDD\Attribute\AggregateRoot;
use PHPMolecules\DDD\Attribute\Entity;
use PHPMolecules\DDD\Attribute\ValueObject;
use PHPMolecules\DDD\Attribute\Service;
use PHPMolecules\DDD\Attribute\Factory;
use PHPMolecules\DDD\Attribute\Repository;
#[Entity]
final class User {}use Tactix\Blacklist;
use Tactix\Check;
use Tactix\YieldViolations;
$check = new Check(new YieldViolations(new Blacklist(Blacklist::DEFAULT_DATA)));
$check->className(User::class);
// or:
$check->folder(__DIR__.'/src');Check throws on violations:
Tactix\ClassViolationExceptionTactix\FolderViolationException
Both exceptions contain a $violations property of type array<Tactix\Violation> to get further details about whether there are missing tags, ambiguity or forbidden relations.
(MyValueObject)-[consumes]->(MyEntity) is a forbidden relation! β
Tactix provides a Symfony Console command tactix:report that creates a static HTML report for a source folder.
# Run the report command for a folder.
bin/console tactix:report <folder>
# or, when installed as a dependency with optional output directory
vendor/bin/console tactix:report <folder> --out-dir=<out-dir>
# Exclude specific namespaces from the report (can be used multiple times)
vendor/bin/console tactix:report <folder> --exclude-namespace="App\\CLI\\" --exclude-namespace="App\\Infrastructure\\"Options:
--out-dir: Base output directory for reports (defaults to project root)--exclude-namespace: Namespace prefix to exclude from the report (can be used multiple times). By default,Doctrine\\,Symfony\\, andPsr\\namespaces are excluded. When you provide your own exclusions, you replace these defaults.
Notes:
- the output files index.html, report.js, chart.js, styles.css are created
- the command prints discovered classes and forbidden relations and finishes with
Report written to: ./report/index.html.
Tactix is framework-agnostic. The core package does not depend on any specific full-stack framework and can be integrated into different environments.
Tactix provides a Symfony Bundle that automatically configures the tactix:report command and provides a Blacklist service via dependency injection.
The bundle is auto-discovered via Symfony Flex. If you're not using Flex or need manual registration:
// config/bundles.php
return [
// ...
Tactix\TactixBundle::class => ['all' => true],
];Create config/packages/tactix.yaml:
tactix:
blacklist:
Entity:
- Factory
- Service
- AggregateRoot
ValueObject:
- Entity
- AggregateRoot
- Repository
- Factory
- Service
AggregateRoot:
- Factory
Repository:
- Factory
- Service
Factory:
- Repository
Service: []Or inherit from defaults by creating an empty config (uses Blacklist::DEFAULT_DATA):
tactix: ~The Blacklist service is automatically available via dependency injection:
use Tactix\Blacklist;
use Tactix\YieldViolations;
final class MyArchitectureValidator
{
public function __construct(private Blacklist $blacklist) {}
public function validateFolder(string $folder): void
{
$violations = new YieldViolations($this->blacklist);
$results = iterator_to_array($violations->fromFolder($folder));
if (!empty($results)) {
throw new \RuntimeException('Architecture violations found!');
}
}
}Or create a Check instance for convenient validation:
$check = new Check(new YieldViolations($this->blacklist));
$check->className(MyClass::class); // throws on violationTactix can be integrated into Laravel projects with a custom service provider.
<?php
// app/Providers/BlacklistProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Tactix\Blacklist;
use Tactix\BlacklistFactory;
class BlacklistProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(Blacklist::class, function () {
$configPath = config_path('tactix.yaml');
return file_exists($configPath)
? BlacklistFactory::fromYamlFile($configPath)
: BlacklistFactory::default();
});
}
}Add to config/app.php:
'providers' => [
// ...
App\Providers\BlacklistProvider::class,
],Create config/tactix.yaml (same format as Symfony):
tactix:
blacklist:
Entity:
- Factory
- Service
- AggregateRoot
# ... rest of configurationuse Tactix\Blacklist;
use Tactix\YieldViolations;
class ArchitectureCommand extends Command
{
public function handle(Blacklist $blacklist)
{
$violations = new YieldViolations($blacklist);
$results = iterator_to_array($violations->fromFolder(app_path()));
$this->info(count($results) . ' violations found');
}
}For CLI scripts or standalone applications:
use Tactix\Blacklist;
use Tactix\BlacklistFactory;
use Tactix\Check;
use Tactix\YieldViolations;
use Symfony\Component\Yaml\Yaml;
// Load blacklist from YAML
$yaml = Yaml::parseFile('tactix.yaml');
$blacklist = BlacklistFactory::fromYaml($yaml);
// Create, check and validate
$check = new Check(new YieldViolations($blacklist));
$check->folder('./src');See CONTRIBUTING.md for details.
