Skip to content

Tech Stack & Principles

Core Technology Stack

Backend

Technology Version Purpose
PHP 8.3+ Programming language
Symfony 8.x Framework
Doctrine ORM 3.x Database abstraction
MySQL/MariaDB 8.x / 10.x Database

Frontend

Technology Purpose
Twig Server-side templating
Vanilla JS Client-side interactivity
Vanilla CSS Styling (no frameworks)

Admin Interface

  • Custom Twig templates - Full control, no admin bundles (EasyAdmin, Sonata)
  • Built specifically for business requirements

Testing

Tool Purpose
Behat BDD functional testing with Gherkin
PHPUnit Unit testing for complex domain logic
PHPStan Static analysis
PHP-CS-Fixer Code style enforcement

Architecture Principles

Clean Architecture

Each island follows clean architecture principles:

┌─────────────────────────────────────────┐
│            Presentation Layer           │
│    (Controllers, CLI Commands, API)     │
├─────────────────────────────────────────┤
│            Application Layer            │
│   (Use Cases, Application Services)     │
├─────────────────────────────────────────┤
│              Domain Layer               │
│  (Entities, Value Objects, Interfaces)  │
├─────────────────────────────────────────┤
│           Infrastructure Layer          │
│ (Repositories, External Services, ORM)  │
└─────────────────────────────────────────┘

Rules: - Dependencies point inward (infrastructure depends on domain, not vice versa) - Domain layer has no external dependencies - Interfaces defined in domain, implemented in infrastructure

Adapter Pattern for External Services

All external integrations use the adapter pattern:

// Domain interface
interface VehicleDataProviderInterface
{
    public function getVehicleBrands(): array;
    public function getVehicleModels(string $brandId): array;
    public function getVehicleSpecs(string $vehicleId): VehicleSpecs;
}

// Infrastructure implementations
class DriveRightAdapter implements VehicleDataProviderInterface { }
class WheelSizeAdapter implements VehicleDataProviderInterface { }

Benefits: - Swap providers without changing domain logic - Easy to test with mock implementations - Multiple providers can be combined

Multi-Tenancy Pattern

Shared database with tenant_id column:

// Entity
#[ORM\Entity]
class Customer
{
    #[ORM\ManyToOne(targetEntity: Tenant::class)]
    private Tenant $tenant;
}

// Doctrine filter (applied globally)
class TenantFilter extends SQLFilter
{
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
    {
        return sprintf('%s.tenant_id = %s', $targetTableAlias, $this->tenantId);
    }
}

Tenant resolution: - Subdomain matching: [tenantname].tyrecloud.be - Stored in request attribute - Doctrine filter automatically applied

Repository Pattern

All data access through repositories:

// Domain interface
interface CustomerRepositoryInterface
{
    public function findById(CustomerId $id): ?Customer;
    public function findByEmail(Email $email): ?Customer;
    public function save(Customer $customer): void;
}

// Infrastructure implementation
class DoctrineCustomerRepository implements CustomerRepositoryInterface
{
    public function __construct(private EntityManagerInterface $em) {}

    public function findById(CustomerId $id): ?Customer
    {
        return $this->em->find(Customer::class, $id);
    }
}

Testing Strategy: BDD with Gherkin

Why BDD?

  1. Living documentation - Feature files document system behavior
  2. Business-readable - Stakeholders can understand and validate specs
  3. Test coverage - Features become executable tests
  4. AI-friendly - Clear specs help AI understand requirements

Feature File Structure

# features/checkout/cart.feature

Feature: Shopping Cart
  As a customer
  I want to add products to my cart
  So that I can purchase them later

  Background:
    Given I am logged in as a B2B customer
    And the following products exist:
      | sku      | name           | price  |
      | TYRE-001 | Michelin PS4   | 150.00 |
      | TYRE-002 | Continental SC | 140.00 |

  Scenario: Add product to empty cart
    Given my cart is empty
    When I add product "TYRE-001" with quantity 4
    Then my cart should contain 1 line item
    And the line item should have quantity 4
    And the cart total should be 600.00

  Scenario: Increase quantity of existing product
    Given my cart contains product "TYRE-001" with quantity 2
    When I add product "TYRE-001" with quantity 2
    Then my cart should contain 1 line item
    And the line item should have quantity 4

Test Layers

Layer Tool Purpose
Acceptance Behat + Gherkin Full feature validation
Integration PHPUnit Repository, service integration
Unit PHPUnit Domain logic, value objects

What to Test at Each Layer

Acceptance (Behat): - User journeys (registration, checkout, order) - Business rules (pricing, discounts, visibility) - Multi-tenant behavior - API endpoints

Integration (PHPUnit): - Repository queries - External service adapters - Event handling

Unit (PHPUnit): - Value objects (Money, Email, VAT number) - Domain entities (state transitions, validation) - Complex calculations (pricing, matching)

Coding Standards

PSR Compliance

  • PSR-1: Basic coding standard
  • PSR-4: Autoloading
  • PSR-12: Extended coding style

Symfony Best Practices

  • Use attributes for routing, ORM mapping
  • Constructor injection for dependencies
  • Final classes by default
  • Typed properties and return types

Naming Conventions

Type Convention Example
Entity Singular noun Customer, Order
Repository EntityRepository CustomerRepository
Service ActionNoun OrderCreator, PriceCalculator
Command VerbNounCommand CreateOrderCommand
Handler CommandHandler CreateOrderHandler
Event PastTenseEvent OrderCreatedEvent
Interface NounInterface CustomerRepositoryInterface

Directory Structure per Island

src/
├── Application/
│   ├── Command/           # CQRS commands
│   ├── Handler/           # Command handlers
│   ├── Query/             # CQRS queries
│   └── Service/           # Application services
├── Domain/
│   ├── Entity/            # Domain entities
│   ├── ValueObject/       # Value objects
│   ├── Repository/        # Repository interfaces
│   ├── Service/           # Domain services
│   └── Event/             # Domain events
├── Infrastructure/
│   ├── Doctrine/          # Doctrine repositories, types
│   ├── Http/              # External API clients
│   └── Messaging/         # Queue handlers
└── Presentation/
    ├── Controller/        # HTTP controllers
    ├── CLI/               # Console commands
    └── API/               # API controllers

Shared Libraries

Creating a Composer Library

atraxion/transsmart-client/
├── src/
│   ├── TranssmartClient.php
│   ├── Request/
│   │   └── ShippingRateRequest.php
│   ├── Response/
│   │   └── ShippingRateResponse.php
│   └── Exception/
│       └── TranssmartException.php
├── tests/
├── composer.json
└── README.md

composer.json:

{
    "name": "atraxion/transsmart-client",
    "type": "library",
    "require": {
        "php": "^8.3",
        "guzzlehttp/guzzle": "^7.0"
    },
    "autoload": {
        "psr-4": {
            "Atraxion\\Transsmart\\": "src/"
        }
    }
}

Creating a Symfony Bundle

atraxion/article-bundle/
├── src/
│   ├── AtraxionArticleBundle.php
│   ├── Entity/
│   │   ├── Article.php
│   │   └── ArticleBrand.php
│   ├── Repository/
│   │   └── ArticleRepositoryInterface.php
│   └── Service/
│       └── ArticleMatchingService.php
├── config/
│   └── services.yaml
├── tests/
└── composer.json

Development Workflow

  1. Write Gherkin feature - Define behavior in feature file
  2. Implement step definitions - Make tests runnable
  3. Write failing tests - TDD approach
  4. Implement feature - Make tests pass
  5. Refactor - Clean up while tests stay green
  6. Code review - PR review before merge

Environment Configuration

Environment Variables

# Database
DATABASE_URL=mysql://user:pass@localhost:3306/webshop

# Tenant
DEFAULT_TENANT_DOMAIN=tyrecloud.be

# External services
TRANSSMART_API_KEY=xxx
PAYNL_API_TOKEN=xxx
PAYNL_SERVICE_ID=xxx
DRIVERIGHT_API_KEY=xxx
ARTEEL_API_KEY=xxx

# ERP Middleware
ERP_MIDDLEWARE_URL=https://erp.internal/api
ERP_MIDDLEWARE_API_KEY=xxx

Configuration per Environment

  • dev: Local development, debug enabled
  • test: Automated testing, fixtures
  • staging: Pre-production testing
  • prod: Production, optimized