PHPackages                             yggdrasilcloud/core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. yggdrasilcloud/core

ActiveProject[API Development](/categories/api)

yggdrasilcloud/core
===================

YggdrasilCloud Core API - Photo management backend with hexagonal architecture

v0.11.0(4mo ago)049[1 PRs](https://github.com/YggdrasilCloud/core/pulls)1proprietaryPHPPHP ^8.4CI passing

Since Oct 18Pushed 4mo agoCompare

[ Source](https://github.com/YggdrasilCloud/core)[ Packagist](https://packagist.org/packages/yggdrasilcloud/core)[ RSS](/packages/yggdrasilcloud-core/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (33)Versions (32)Used By (1)

YggdrasilCloud API
==================

[](#yggdrasilcloud-api)

REST API for photo management built with Domain-Driven Design (DDD) architecture using Symfony 7.3.

Features
--------

[](#features)

- 🏗️ **Domain-Driven Design**: Clean architecture with bounded contexts
- 📸 **Photo Management**: Upload, organize, and list photos by folders
- 🔒 **File Validation**: Configurable file size limits and MIME type restrictions
- 🧪 **100% Mutation Coverage**: Comprehensive test suite with Infection
- 🐳 **Docker Ready**: FrankenPHP + PostgreSQL with Docker Compose
- 🚀 **CQRS Pattern**: Separate commands and queries with Symfony Messenger

Tech Stack
----------

[](#tech-stack)

- **Framework**: Symfony 7.3
- **Runtime**: FrankenPHP (PHP 8.4 + Caddy)
- **Database**: PostgreSQL 16
- **Testing**: PHPUnit 12 + Infection
- **Architecture**: DDD with CQRS

Project Structure
-----------------

[](#project-structure)

```
src/
└── Photo/                           # Photo bounded context
    ├── Application/
    │   ├── Command/                # Commands (write operations)
    │   │   ├── CreateFolder/
    │   │   └── UploadPhotoToFolder/
    │   └── Query/                  # Queries (read operations)
    │       └── ListPhotosInFolder/
    ├── Domain/
    │   ├── Model/                  # Aggregates and Value Objects
    │   │   ├── Photo.php
    │   │   ├── Folder.php
    │   │   ├── PhotoId.php
    │   │   ├── FileName.php
    │   │   ├── FolderName.php
    │   │   └── StoredFile.php
    │   ├── Event/                  # Domain Events
    │   ├── Repository/             # Repository Interfaces
    │   └── Service/
    │       └── FileValidator.php   # File validation service
    ├── Infrastructure/
    │   ├── Persistence/Doctrine/   # Doctrine ORM implementation
    │   └── Storage/                # File storage implementation
    └── UserInterface/
        └── Http/Controller/        # REST API controllers

```

Getting Started
---------------

[](#getting-started)

### Prerequisites

[](#prerequisites)

- Docker and Docker Compose
- Git

### Installation

[](#installation)

1. Clone the repository:

```
git clone git@github.com:YggdrasilCloud/core.git
cd core
```

2. Start the services:

```
docker compose up -d
```

3. Create the database and run migrations:

```
docker compose exec php bin/console doctrine:database:create
docker compose exec php bin/console doctrine:migrations:migrate -n
```

4. The API is now available at `http://localhost:8000`

Optional Packages
-----------------

[](#optional-packages)

YggdrasilCloud uses a modular architecture. Some features are available as optional packages:

### Storage S3 (AWS S3 / MinIO)

[](#storage-s3-aws-s3--minio)

For S3-compatible storage instead of local filesystem:

```
composer require yggdrasilcloud/storage-s3
```

Then configure your `.env`:

```
STORAGE_DSN="storage://s3?bucket=my-bucket&region=eu-west-1"
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
```

See [Storage DSN Configuration](#storage-dsn-configuration) for more details.

API Endpoints
-------------

[](#api-endpoints)

### Health Check

[](#health-check)

```
GET /health
GET /health/ready
```

### Folders

[](#folders)

```
# Create a folder
POST /api/folders
Content-Type: application/json

{
  "name": "Vacances 2025"
}

# Response: 201 Created
{
  "id": "01936ef5-8f6a-7f3e-b9c6-0242ac120002",
  "name": "Vacances 2025",
  "createdAt": "2025-10-11T12:00:00+00:00"
}
```

### Photos

[](#photos)

```
# Upload a photo to a folder
POST /api/folders/{folderId}/photos
Content-Type: multipart/form-data

file:

# Response: 201 Created
{
  "id": "01936ef6-a2b4-7890-1234-0242ac120003",
  "fileName": "photo.jpg",
  "mimeType": "image/jpeg",
  "sizeInBytes": 2048576
}

# List photos in a folder
GET /api/folders/{folderId}/photos?page=1&perPage=50

# Response: 200 OK
{
  "items": [
    {
      "id": "01936ef6-a2b4-7890-1234-0242ac120003",
      "fileName": "photo.jpg",
      "storagePath": "/storage/photos/...",
      "mimeType": "image/jpeg",
      "sizeInBytes": 2048576,
      "uploadedAt": "2025-10-11T12:05:00+00:00"
    }
  ],
  "total": 1,
  "page": 1,
  "perPage": 50
}
```

Configuration
-------------

[](#configuration)

### Environment Variables

[](#environment-variables)

```
# Database
DATABASE_URL="postgresql://app:secret@postgres:5432/app?serverVersion=16&charset=utf8"

# Photo Upload Settings
PHOTO_MAX_FILE_SIZE=20971520                                    # 20MB (-1 = unlimited)
PHOTO_ALLOWED_MIME_TYPES="image/jpeg,image/png,image/gif,image/webp"

# Storage (DSN-based configuration - recommended)
STORAGE_DSN="storage://local?root=%kernel.project_dir%/var/storage&max_key_length=1024&max_component_length=255"
```

### Storage DSN Configuration

[](#storage-dsn-configuration)

The storage system uses a flexible DSN-based configuration that allows you to switch between different storage backends (local filesystem, S3, FTP, etc.) by simply changing an environment variable.

#### DSN Format

[](#dsn-format)

```
storage://?=&=

```

#### Built-in Driver: Local Filesystem

[](#built-in-driver-local-filesystem)

**Basic usage:**

```
STORAGE_DSN="storage://local?root=/var/storage"
```

**With custom limits:**

```
STORAGE_DSN="storage://local?root=/var/storage&max_key_length=512&max_component_length=200"
```

**Options:**

- `root` (required): Base directory for file storage
- `max_key_length` (optional, default: 1024): Maximum total key length in characters
- `max_component_length` (optional, default: 255): Maximum path component length (filesystem limit)

#### External Drivers via Bridges

[](#external-drivers-via-bridges)

For cloud storage or other backends, install the corresponding bridge package:

**AWS S3 / MinIO:**

```
composer require yggdrasilcloud/storage-s3
```

```
STORAGE_DSN="storage://s3?bucket=my-bucket&region=eu-west-1"
```

**Note:** Set your S3 credentials using the standard environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. Do **not** include credentials in the DSN.

**FTP/FTPS:**

```
composer require yggdrasilcloud/storage-ftp
```

```
STORAGE_DSN="storage://ftp?host=ftp.example.com&username=user&password=pass&port=21"
```

**Google Cloud Storage:**

```
composer require yggdrasilcloud/storage-gcs
```

```
STORAGE_DSN="storage://gcs?bucket=my-bucket&projectId=my-project&keyFilePath=/path/to/key.json"
```

#### Bridge Auto-Discovery

[](#bridge-auto-discovery)

Storage bridges are automatically discovered via Symfony's service tag `storage.bridge`. When you install a bridge package, it registers itself automatically—no manual configuration needed.

**Missing Bridge Error:**

If you try to use a storage driver without installing its bridge, you'll see:

```
No storage adapter found for driver "s3".
To use this driver, install the corresponding bridge package:
composer require yggdrasilcloud/storage-s3.
See https://github.com/YggdrasilCloud/core#storage-bridges for available bridges.

```

#### Creating Custom Bridges

[](#creating-custom-bridges)

To create your own storage bridge (e.g., for Azure Blob, Dropbox, etc.), implement `StorageBridgeInterface` and tag your service:

```
# config/services.yaml
App\Infrastructure\Storage\Bridge\AzureBridge:
    tags:
        - { name: storage.bridge }
```

```
use App\File\Infrastructure\Storage\Bridge\StorageBridgeInterface;
use App\File\Infrastructure\Storage\StorageConfig;
use App\File\Domain\Port\FileStorageInterface;

final class AzureBridge implements StorageBridgeInterface
{
    public function supports(string $driver): bool
    {
        return $driver === 'azure';
    }

    public function create(StorageConfig $config): FileStorageInterface
    {
        $account = $config->get('account');
        $container = $config->get('container');

        return new AzureStorage($account, $container);
    }
}
```

Then use it:

```
STORAGE_DSN="storage://azure?account=myaccount&container=photos"
```

#### Dependency Injection Configuration

[](#dependency-injection-configuration)

The storage system integrates seamlessly with Symfony's DI container using a factory pattern:

**Service Configuration (`config/services.yaml`):**

```
# Storage Infrastructure - DSN Parser
App\File\Infrastructure\Storage\StorageDsnParser: ~

# Storage Infrastructure - Factory with Bridge Auto-Discovery
App\File\Infrastructure\Storage\StorageFactory:
    arguments:
        $bridges: !tagged_iterator storage.bridge

# Storage Interface - Created via Factory from DSN
App\File\Domain\Port\FileStorageInterface:
    factory: ['@App\File\Infrastructure\Storage\StorageFactory', 'create']
    arguments:
        $dsn: '%env(STORAGE_DSN)%'
```

**How it works:**

1. **StorageFactory** receives all services tagged with `storage.bridge` via `!tagged_iterator`
2. **FileStorageInterface** is created by calling `StorageFactory::create()` with the `STORAGE_DSN` environment variable
3. The factory parses the DSN and either:
    - Returns a built-in adapter (e.g., `LocalStorage` for `local://`)
    - Searches registered bridges for external drivers (e.g., S3, FTP)
4. The resolved storage adapter is injected wherever `FileStorageInterface` is type-hinted

**Usage in your code:**

```
use App\File\Domain\Port\FileStorageInterface;

final class MyService
{
    public function __construct(
        private FileStorageInterface $storage,
    ) {}

    public function uploadFile($stream, string $key): void
    {
        $this->storage->save($stream, $key, 'image/jpeg', -1);
    }
}
```

No need to know which storage backend is used—switch from local to S3 by simply changing `STORAGE_DSN`!

**Optional: Logger Integration**

LocalStorage supports optional PSR-3 logging for I/O errors (Monolog, etc.):

```
App\File\Infrastructure\Storage\Adapter\LocalStorage:
    arguments:
        $logger: '@monolog.logger'
```

When configured, I/O errors (file not found, write failures, etc.) are automatically logged with context.

Development
-----------

[](#development)

### Run Tests

[](#run-tests)

```
# Unit tests
docker compose exec php vendor/bin/phpunit

# Mutation testing (100% MSI required)
docker compose exec php vendor/bin/infection
```

### Code Quality

[](#code-quality)

```
# PHP CS Fixer (if configured)
docker compose exec php vendor/bin/php-cs-fixer fix

# PHPStan (if configured)
docker compose exec php vendor/bin/phpstan analyse
```

### Database Migrations

[](#database-migrations)

```
# Create a new migration
docker compose exec php bin/console make:migration

# Run migrations
docker compose exec php bin/console doctrine:migrations:migrate
```

Testing
-------

[](#testing)

The project has comprehensive test coverage:

- **72 unit tests** covering Value Objects, Aggregates, and Services
- **100% Mutation Score Indicator (MSI)** with Infection
- **114 assertions** ensuring edge cases and boundaries

### Test Structure

[](#test-structure)

```
tests/
└── Unit/
    └── Photo/
        ├── Domain/
        │   ├── Model/
        │   │   ├── PhotoIdTest.php        # UUID v7 generation
        │   │   ├── FileNameTest.php       # Filename validation
        │   │   ├── FolderNameTest.php     # Folder name validation
        │   │   ├── StoredFileTest.php     # File metadata validation
        │   │   ├── PhotoTest.php          # Photo aggregate
        │   │   └── FolderTest.php         # Folder aggregate
        │   └── Service/
        │       └── FileValidatorTest.php  # File validation service

```

### Run Specific Tests

[](#run-specific-tests)

```
# Run all tests
docker compose exec php vendor/bin/phpunit

# Run specific test class
docker compose exec php vendor/bin/phpunit tests/Unit/Photo/Domain/Model/PhotoTest.php

# Run with coverage
docker compose exec php vendor/bin/phpunit --coverage-html coverage
```

Architecture Decisions
----------------------

[](#architecture-decisions)

### Why Domain-Driven Design?

[](#why-domain-driven-design)

- **Clear boundaries**: Photo context is isolated and can evolve independently
- **Business logic in domain**: Rules like "photos must be images" are in the domain
- **Testability**: Domain logic is pure PHP, easy to test without framework
- **Flexibility**: Can swap infrastructure (Doctrine → another ORM) without touching domain

### Why CQRS?

[](#why-cqrs)

- **Separation of concerns**: Commands (write) vs Queries (read)
- **Scalability**: Can optimize read and write models independently
- **Event sourcing ready**: Commands emit domain events for future event store

### Why Value Objects?

[](#why-value-objects)

- **Type safety**: `PhotoId` instead of raw strings prevents errors
- **Validation**: Business rules enforced at construction (filename max 255 chars)
- **Immutability**: Value objects can't be changed after creation

### Why separate repositories for read/write?

[](#why-separate-repositories-for-readwrite)

- **CQRS pattern**: Commands use aggregate repositories, queries use read-optimized repositories
- **Performance**: Read models can be denormalized for faster queries
- **Evolution**: Read and write models can evolve separately

Domain Concepts
---------------

[](#domain-concepts)

### Aggregates

[](#aggregates)

- **Photo**: Represents an uploaded photo with metadata
- **Folder**: Groups photos together

### Value Objects

[](#value-objects)

- **PhotoId / FolderId**: UUID v7 identifiers
- **FileName**: Validated filename (max 255 chars, trims whitespace)
- **FolderName**: Validated folder name (max 255 chars, non-empty)
- **StoredFile**: File metadata (path, MIME type, size)

### Domain Events

[](#domain-events)

- **PhotoUploaded**: Emitted when a photo is uploaded
- **FolderCreated**: Emitted when a folder is created

These events are currently stored in-memory but can be persisted with the Transactional Outbox pattern (see code comments).

Future Enhancements
-------------------

[](#future-enhancements)

### Documented in Code

[](#documented-in-code)

- **Transactional Outbox Pattern**: Store domain events in database for reliable publishing
- **Duplicate Detection**: Use SHA-256 hash to detect duplicate uploads
- **EXIF Metadata**: Extract and store camera, location, date taken

### Planned Features

[](#planned-features)

- **User Authentication**: JWT-based authentication
- **Authorization**: Role-based access control (RBAC)
- **Photo Sharing**: Share folders with other users
- **Search**: Full-text search on filenames and EXIF data
- **Thumbnails**: Generate multiple sizes for responsive images
- **Albums**: Virtual collections across folders

Docker Services
---------------

[](#docker-services)

### FrankenPHP (php)

[](#frankenphp-php)

- PHP 8.4 with FrankenPHP (Caddy + PHP)
- Exposed on port 8000
- Hot-reload with volume mount

### PostgreSQL (postgres)

[](#postgresql-postgres)

- PostgreSQL 16
- Data persisted in `postgres-data` volume
- Exposed on port 5432

CORS Configuration
------------------

[](#cors-configuration)

For multi-client support (web, mobile), CORS is configured to accept requests from:

- Web frontend (localhost:5173 in dev, production domain)
- Mobile apps (Android, iOS)

Contributing
------------

[](#contributing)

### Commit Style

[](#commit-style)

Simple, descriptive commit messages (no conventional commits):

```
Add Photo domain with DDD architecture
Configure Infection for mutation testing

```

### Branch Strategy

[](#branch-strategy)

- `main`: Stable, production-ready code
- Feature branches: Create from `main`, merge via PR

License
-------

[](#license)

MIT

Related Repositories
--------------------

[](#related-repositories)

- **Frontend**: [YggdrasilCloud/frontend](https://github.com/YggdrasilCloud/frontend) - SvelteKit web application
- **Android**: (Coming soon)

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance76

Regular maintenance activity

Popularity8

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

28

Last Release

133d ago

PHP version history (2 changes)v0.1.0PHP &gt;=8.3

v0.4.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/a864b67689c18f73803e34b6c4e269ab08395067ad635e1382ed96cc76f596a0?d=identicon)[roukmoute](/maintainers/roukmoute)

---

Top Contributors

[![roukmoute](https://avatars.githubusercontent.com/u/2140469?v=4)](https://github.com/roukmoute "roukmoute (292 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/yggdrasilcloud-core/health.svg)

```
[![Health](https://phpackages.com/badges/yggdrasilcloud-core/health.svg)](https://phpackages.com/packages/yggdrasilcloud-core)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M651](/packages/sylius-sylius)[kimai/kimai

Kimai - Time Tracking

4.6k7.4k1](/packages/kimai-kimai)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.3M152](/packages/sulu-sulu)[shopware/platform

The Shopware e-commerce core

3.3k1.5M3](/packages/shopware-platform)[contao/core-bundle

Contao Open Source CMS

1231.6M2.4k](/packages/contao-core-bundle)[prestashop/prestashop

PrestaShop is an Open Source e-commerce platform, committed to providing the best shopping cart experience for both merchants and customers.

9.0k15.4k](/packages/prestashop-prestashop)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
