Skip to content

Common

The Common package provides essential utilities: assertions, custom exceptions, and value objects.

Assert

Assertion system wrapping Webmozart Assert with custom message support.

Basic Usage

use Atournayre\Common\Assert\Assert;

// String validation
Assert::stringNotEmpty(value: $name, message: 'Name cannot be empty');
Assert::minLength(value: $password, min: 8, message: 'Password too short');

// Numeric validation
Assert::greaterThan(value: $age, limit: 0, message: 'Age must be positive');
Assert::range(value: $score, min: 0, max: 100);

// Array validation
Assert::notEmpty(value: $items, message: 'Items cannot be empty');
Assert::allIsInstanceOf(value: $users, class: User::class);

// Type validation
Assert::isInstanceOf(value: $user, class: User::class);
Assert::uuid(value: $id, message: 'Invalid UUID format');

Common Assertions

// Null/NotNull
Assert::notNull(value: $value);
Assert::null(value: $value);

// Boolean
Assert::true(value: $condition);
Assert::false(value: $condition);

// String
Assert::string(value: $text);
Assert::email(value: $email);
Assert::url(value: $website);
Assert::startsWith(value: $text, prefix: 'https://');
Assert::endsWith(value: $filename, suffix: '.php');

// Numeric
Assert::integer(value: $count);
Assert::float(value: $price);
Assert::numeric(value: $value);
Assert::positiveInteger(value: $quantity);

// Array
Assert::isArray(value: $data);
Assert::keyExists(array: $data, key: 'id');
Assert::count(array: $items, number: 5);
Assert::minCount(array: $items, min: 1);

// Object
Assert::object(value: $entity);
Assert::classExists(value: MyClass::class);
Assert::implementsInterface(value: $service, interface: ServiceInterface::class);

Collection Assertions

// All elements must satisfy
Assert::allString(value: $names);
Assert::allInteger(value: $numbers);
Assert::allIsInstanceOf(value: $entities, class: Entity::class);

// At least one element must satisfy
Assert::inArray(value: $status, array: ['active', 'pending']);

Exceptions

Custom exception hierarchy with fluent interfaces.

Hierarchy

use Atournayre\Common\Exception\InvalidArgumentException;
use Atournayre\Common\Exception\RuntimeException;
use Atournayre\Common\Exception\UnexpectedValueException;
use Atournayre\Common\Exception\BadMethodCallException;
use Atournayre\Common\Exception\MutableException;
use Atournayre\Common\Exception\NullException;

InvalidArgumentException

use Atournayre\Common\Exception\InvalidArgumentException;

// Simple creation
throw InvalidArgumentException::new(message: 'Invalid value provided', code: 0);

// With previous exception
$exception = InvalidArgumentException::new(message: 'Invalid value')
    ->withPrevious(previous: $previousException);

// From existing throwable
$exception = InvalidArgumentException::fromThrowable(throwable: $existingException);

// Throw with optional logging
InvalidArgumentException::new(message: 'Error')->throw(logger: $logger, context: ['key' => 'value']);

RuntimeException

use Atournayre\Common\Exception\RuntimeException;

// Same API as InvalidArgumentException
throw RuntimeException::new(message: 'Operation failed', code: 0);

// With previous exception
throw RuntimeException::new(message: 'Database error')
    ->withPrevious(previous: $dbException);

UnexpectedValueException

use Atournayre\Common\Exception\UnexpectedValueException;

// Same API as other exceptions
throw UnexpectedValueException::new(message: 'Unexpected status', code: 0);

MutableException

use Atournayre\Common\Exception\MutableException;

// To indicate an object should not be modified
throw MutableException::new(message: 'Cannot modify immutable object');

NullException

use Atournayre\Common\Exception\NullException;

// For unexpected null values
throw NullException::new(message: 'Value cannot be null');

ThrowableTrait

Trait for creating custom exceptions. Provides consistent API for all exceptions.

use Atournayre\Common\Exception\ThrowableTrait;
use Atournayre\Contracts\Exception\ThrowableInterface;

class CustomException extends \Exception implements ThrowableInterface
{
    use ThrowableTrait;
}

// Usage - Available methods from trait:

// Create new instance
throw CustomException::new(message: 'Custom error', code: 500);

// Chain with previous exception
throw CustomException::new(message: 'Error')
    ->withPrevious(previous: $previousException);

// Create from existing throwable
throw CustomException::fromThrowable(throwable: $existingException);

// Throw with logging
CustomException::new(message: 'Error')->throw(
    logger: $logger,
    context: ['user_id' => 123]
);

Value Objects

Context

Immutable context object for logging with user and timestamp:

use Atournayre\Common\VO\Context\Context;
use Atournayre\Contracts\Security\UserInterface;

// Creation
$context = Context::create(
    user: $user, // UserInterface
    createdAt: new \DateTimeImmutable()
);

// Access
$user = $context->user();
$createdAt = $context->createdAt();

// For logging (implements LoggableInterface)
$logData = $context->toLog();
// Returns: ['user' => 'user_identifier', 'createdAt' => '2025-01-15 10:30:00']

// Null context
$nullContext = Context::asNull();

Duration

Represents a time duration in milliseconds:

use Atournayre\Common\VO\Duration;

// Creation (always from milliseconds)
$duration = Duration::of(milliseconds: 3600000); // 1 hour

// Access
$ms = $duration->milliseconds(); // 3600000
$ms = $duration->asIs(); // same as milliseconds()
$seconds = $duration->inSeconds(); // 3600.0
$minutes = $duration->inMinutes(); // 60.0
$hours = $duration->inHours(); // 1.0
$days = $duration->inDays(); // 0.041666...

// Human readable format
$formatted = $duration->humanReadable(); // "1 hour"
$formatted = $duration->humanReadable(glue: ', '); // "1 hour"

Memory

Represents a memory amount in bytes:

use Atournayre\Common\VO\Memory;

// Creation (always from bytes)
$memory = Memory::fromBytes(bytes: 1048576); // 1 MB

// Access
$bytes = $memory->asIs(); // 1048576
$kb = $memory->inKilobytes(); // 1024.0
$mb = $memory->inMegabytes(); // 1.0
$gb = $memory->inGigabytes(); // 0.0009765625
$tb = $memory->inTerabytes(); // 0.00000095367...

// Comparison
$isEqual = $memory->equalsTo(size: 1048576); // BoolEnum

// Human readable format
$formatted = $memory->humanReadable(); // "1.00 MB"

Uri

Value object for URIs implementing PSR-7 UriInterface:

use Atournayre\Common\VO\Uri;

// Creation
$uri = Uri::of(uri: 'https://user:pass@example.com:8080/path?query=value#fragment');

// Access components
$scheme = $uri->scheme();        // "https"
$authority = $uri->authority();  // "user:pass@example.com:8080"
$userInfo = $uri->userInfo();    // "user:pass"
$host = $uri->host();            // "example.com"
$port = $uri->port();            // 8080
$path = $uri->path();            // "/path"
$query = $uri->query();          // "query=value"
$fragment = $uri->fragment();    // "fragment"

// Immutable modifications (PSR-7)
$newUri = $uri->withScheme(scheme: 'http');
$newUri = $uri->withHost(host: 'newdomain.com');
$newUri = $uri->withPath(path: '/new/path');
$newUri = $uri->withQuery(query: 'key=value');
$newUri = $uri->withUserInfo(user: 'username');
$newUri = $uri->withUserAndPassword(user: 'user', password: 'pass');

// Conversion
$string = $uri->toString();
$string = (string) $uri; // __toString()

PlainPassword

Simple wrapper around StringType for plain text passwords:

use Atournayre\Common\VO\Security\PlainPassword;
use Atournayre\Primitives\StringType;

// Creation (wraps StringType)
$password = PlainPassword::of(value: StringType::of(value: 'MyPassword123'));

// StringType methods available via StringTypeTrait
$length = $password->length();
$isEmpty = $password->isEmpty();

// Null password
$nullPassword = PlainPassword::asNull();
$isNull = $nullPassword->isNull();

// Note: PlainPassword is just a StringType wrapper
// Use Symfony PasswordHasher for actual hashing/verification

Event (Deprecated)

Warning

DEPRECATED: This class is deprecated and will be removed in a future version. Use the Domain Events Management system with AbstractCommandEvent/AbstractQueryEvent instead. See Domain Events.

use Atournayre\Common\VO\Event;
use Symfony\Component\Messenger\MessageBusInterface;

// Event extends and uses ContextTrait (not a simple factory)
// Must be extended, cannot be used directly

class UserCreatedEvent extends Event
{
    // Custom implementation
}

$event = new UserCreatedEvent();

// Available methods from Event:
$identifier = $event->_identifier(); // spl_object_hash
$type = $event->_type(); // static::class
$event->stopPropagation();
$isStopped = $event->isPropagationStopped(); // bool

// Dispatch via message bus
$event->dispatch(messageBus: $messageBus);

// Logging
$logData = $event->toLog(); // ['identifier' => '...', 'type' => '...']

Domain Events

New event classes:

use Atournayre\Common\AbstractCommandEvent;
use Atournayre\Common\AbstractQueryEvent;

// Command event
final class UserCreatedEvent extends AbstractCommandEvent
{
    private function __construct(private readonly User $user) {}

    public static function new(User $user): self
    {
        return new self(user: $user);
    }

    public function user(): User
    {
        return $this->user;
    }
}

// Query event
final class UserFoundEvent extends AbstractQueryEvent
{
    private function __construct(private readonly User $user) {}

    public static function new(User $user): self
    {
        return new self(user: $user);
    }

    public function user(): User
    {
        return $this->user;
    }
}

Usage Patterns

Validation with Assert

class UserService
{
    public function createUser(string $email, string $password): User
    {
        Assert::email(value: $email, message: 'Invalid email format');
        Assert::minLength(value: $password, min: 8, message: 'Password too short');

        return User::new(email: $email, password: $password);
    }
}

Error Handling with Exceptions

class PaymentService
{
    public function processPayment(Payment $payment): void
    {
        if ($payment->amount()->isNegative()) {
            InvalidArgumentException::new(message: 'Amount must be positive')
                ->throw(logger: $this->logger, context: ['amount' => $payment->amount()->value()]);
        }

        try {
            $this->gateway->charge($payment);
        } catch (GatewayException $e) {
            RuntimeException::new(message: 'Payment processing failed')
                ->withPrevious(previous: $e)
                ->throw(logger: $this->logger, context: ['payment_id' => $payment->id()]);
        }
    }
}

Context for Logging

$context = Context::create(
    user: $user,
    createdAt: new \DateTimeImmutable()
);

$logger->info(message: 'User logged in', context: $context->toLog());
// Logs: ['user' => 'user_identifier', 'createdAt' => '2025-01-15 10:30:00']

Tests

use PHPUnit\Framework\TestCase;
use Atournayre\Common\Assert\Assert;
use Atournayre\Common\Exception\InvalidArgumentException;

class AssertTest extends TestCase
{
    public function testStringNotEmpty(): void
    {
        $this->expectException(InvalidArgumentException::class);
        Assert::stringNotEmpty(value: '', message: 'String cannot be empty');
    }

    public function testEmail(): void
    {
        Assert::email(value: 'test@example.com');
        $this->assertTrue(true);
    }
}