PHPackages                             awd-studio/vo-optional-php - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. awd-studio/vo-optional-php

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

awd-studio/vo-optional-php
==========================

Type-safe Optional value object for PHP 8.4+. A robust implementation of the Optional pattern inspired by Java, providing elegant null-safety and functional programming capabilities.

v1.0.1(1mo ago)00MITPHPPHP ^8.4

Since Feb 19Pushed 2mo agoCompare

[ Source](https://github.com/awd-studio/vo-optional-php)[ Packagist](https://packagist.org/packages/awd-studio/vo-optional-php)[ Docs](https://github.com/awd-studio/vo-optional-php)[ RSS](/packages/awd-studio-vo-optional-php/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (12)Versions (3)Used By (0)

Optional Value Object for PHP 8.4+
==================================

[](#optional-value-object-for-php-84)

[![PHP Version](https://camo.githubusercontent.com/02463ad42fbbb8e930dc93f83e8b2ecd9ad3f718d33bb429f5f8f792f9cfd2e5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e342d626c75652e737667)](https://www.php.net/)[![License](https://camo.githubusercontent.com/8bb50fd2278f18fc326bf71f6e88ca8f884f72f179d3e555e20ed30157190d0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e737667)](LICENSE)[![Tests](https://github.com/awd-studio/vo-optional-php/workflows/CI/badge.svg)](https://github.com/awd-studio/vo-optional-php/actions)

A robust, type-safe implementation of the Optional pattern for PHP 8.4+, inspired by [Java's Optional class](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html). This library provides a container object which may or may not contain a non-null value, helping you write cleaner, more expressive code with better null safety.

Inspiration
-----------

[](#inspiration)

This implementation follows the design patterns from:

- [Java Optional (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html)
- [Optional Pattern on Wikipedia](https://en.wikipedia.org/wiki/Option_type)
- [Functional Programming Principles](https://en.wikipedia.org/wiki/Monad_(functional_programming))

Features
--------

[](#features)

- **Type-Safe**: Full generic type support with PHPStan and Psalm annotations
- **Immutable**: All operations return new instances, ensuring thread-safety
- **PHP 8.4+**: Leverages modern PHP features (readonly classes, mixed types)
- **Zero Dependencies**: Lightweight with no external runtime dependencies
- **Fully Tested**: Comprehensive test coverage with 77 tests
- **Fluent API**: Chainable methods for elegant functional programming
- **Well-Documented**: Complete PHPDoc annotations and inline documentation

Installation
------------

[](#installation)

```
composer require awd-studio/vo-optional-php
```

Requirements
------------

[](#requirements)

- PHP 8.4 or higher

Quick Start
-----------

[](#quick-start)

```
use Awd\ValueObject\Optional;

// Create an Optional with a value
$optional = Optional::of('Hello, World!');

// Create an Optional that may be null
$optional = Optional::ofNullable($possiblyNullValue);

// Create an empty Optional
$optional = Optional::empty();
```

Usage Examples
--------------

[](#usage-examples)

### Basic Value Retrieval

[](#basic-value-retrieval)

```
use Awd\ValueObject\Optional;

// Get value or throw exception
$value = Optional::of('test')->get(); // 'test'

// Get value with fallback
$value = Optional::empty()->orElse('default'); // 'default'

// Get value with lazy fallback
$value = Optional::empty()->orElseGet(fn() => expensiveOperation());

// Get value or return null
$value = Optional::empty()->orNull(); // null

// Get value or throw custom exception
$value = Optional::empty()->orElseThrow(fn() => new CustomException());
```

### Working with Domain Entities

[](#working-with-domain-entities)

```
use Awd\ValueObject\Optional;

class User {
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly ?string $email = null
    ) {}
}

class UserRepository {
    public function findById(int $id): Optional {
        $user = $this->db->find($id);
        return Optional::ofNullable($user);
    }
}

// Safe user retrieval
$userName = $repository->findById(123)
    ->map(fn(User $user) => $user->name)
    ->orElse('Guest');

// Chain operations on entities
$userEmail = $repository->findById(123)
    ->map(fn(User $user) => $user->email)
    ->filter(fn(?string $email) => null !== $email)
    ->map(fn(string $email) => strtolower($email))
    ->orElse('no-reply@example.com');
```

### Value Object Transformations

[](#value-object-transformations)

```
use Awd\ValueObject\Optional;

class Email {
    private function __construct(public readonly string $value) {}

    public static function fromString(string $email): Optional {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return Optional::empty();
        }
        return Optional::of(new self($email));
    }
}

class PhoneNumber {
    private function __construct(public readonly string $value) {}

    public static function fromString(string $phone): Optional {
        if (!preg_match('/^\+?[1-9]\d{1,14}$/', $phone)) {
            return Optional::empty();
        }
        return Optional::of(new self($phone));
    }
}

// Safe value object creation
$email = Email::fromString($input)
    ->map(fn(Email $e) => $e->value)
    ->orElseThrow(fn() => new InvalidArgumentException('Invalid email'));

// Chaining value object operations
$contact = Optional::ofNullable($user->email)
    ->flatMap(fn(string $e) => Email::fromString($e))
    ->or(fn() => Optional::ofNullable($user->phone)
        ->flatMap(fn(string $p) => PhoneNumber::fromString($p)))
    ->orElseThrow(fn() => new ContactRequiredException());
```

### Repository Pattern Integration

[](#repository-pattern-integration)

```
use Awd\ValueObject\Optional;

interface OrderRepository {
    public function findByOrderNumber(string $orderNumber): Optional;
}

class Order {
    public function __construct(
        public readonly string $orderNumber,
        public readonly string $status,
        public readonly array $items
    ) {}

    public function isPaid(): bool {
        return $this->status === 'paid';
    }
}

// Safe order processing
$orderRepository->findByOrderNumber('ORD-123')
    ->filter(fn(Order $order) => $order->isPaid())
    ->ifPresentOrElse(
        fn(Order $order) => $this->shipOrder($order),
        fn() => logger()->warning('Order not found or not paid')
    );
```

### Aggregate Root Operations

[](#aggregate-root-operations)

```
use Awd\ValueObject\Optional;

class Customer {
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        private ?Address $billingAddress = null
    ) {}

    public function getBillingAddress(): Optional {
        return Optional::ofNullable($this->billingAddress);
    }
}

class Address {
    public function __construct(
        public readonly string $street,
        public readonly string $city,
        public readonly string $country
    ) {}
}

// Navigate aggregate roots safely
$country = $customerRepository->findById($customerId)
    ->flatMap(fn(Customer $c) => $c->getBillingAddress())
    ->map(fn(Address $a) => $a->country)
    ->orElse('Unknown');
```

### Service Layer with Optional

[](#service-layer-with-optional)

```
use Awd\ValueObject\Optional;

class NotificationService {
    public function notifyUser(int $userId, string $message): void {
        $this->userRepository->findById($userId)
            ->flatMap(fn(User $user) => $user->getPreferredContact())
            ->ifPresentOrElse(
                fn(Contact $contact) => $this->send($contact, $message),
                fn() => $this->logger->info("No contact method for user {$userId}")
            );
    }
}

class PaymentProcessor {
    public function refund(string $transactionId): Optional {
        return $this->transactionRepository->findById($transactionId)
            ->filter(fn(Transaction $t) => $t->canBeRefunded())
            ->map(fn(Transaction $t) => $this->processRefund($t))
            ->flatMap(fn(RefundResult $r) =>
                $r->isSuccessful()
                    ? Optional::of($r)
                    : Optional::empty()
            );
    }
}
```

### Transformations

[](#transformations)

```
use Awd\ValueObject\Optional;

// Transform value with map()
$result = Optional::of('john')
    ->map(fn($name) => strtoupper($name))
    ->get(); // 'JOHN'

// Chain multiple transformations
$result = Optional::of(5)
    ->map(fn($n) => $n * 2)
    ->map(fn($n) => $n + 10)
    ->get(); // 20

// FlatMap for nested Optionals
$result = Optional::of('user@example.com')
    ->flatMap(fn($email) => validateEmail($email))
    ->flatMap(fn($email) => findUserByEmail($email))
    ->orElse(null);
```

### Conditional Operations

[](#conditional-operations)

```
use Awd\ValueObject\Optional;

// Filter with predicate
$result = Optional::of(25)
    ->filter(fn($age) => $age >= 18)
    ->orElse('underage'); // 25

// Execute action if present
Optional::of($user)
    ->ifPresent(fn($u) => sendEmail($u));

// Execute action or alternative
Optional::ofNullable($config)
    ->ifPresentOrElse(
        fn($cfg) => applyConfig($cfg),
        fn() => useDefaultConfig()
    );
```

### Checking Presence

[](#checking-presence)

```
use Awd\ValueObject\Optional;

$optional = Optional::of('value');

if ($optional->isPresent()) {
    // Value exists
}

if ($optional->isEmpty()) {
    // No value
}
```

### Alternative Optional

[](#alternative-optional)

```
use Awd\ValueObject\Optional;

// Return alternative Optional if empty
$result = Optional::empty()
    ->or(fn() => Optional::of('alternative'))
    ->get(); // 'alternative'

// Chain alternatives
$result = Optional::empty()
    ->or(fn() => tryPrimarySource())
    ->or(fn() => trySecondarySource())
    ->orElse('fallback');
```

### Equality and String Representation

[](#equality-and-string-representation)

```
use Awd\ValueObject\Optional;

$opt1 = Optional::of('test');
$opt2 = Optional::of('test');

$opt1->equals($opt2); // true

echo Optional::of('value')->toString(); // "Optional[value]"
echo Optional::empty()->toString(); // "Optional.empty"
echo Optional::of(42); // "Optional[42]" (uses __toString)
```

API Reference
-------------

[](#api-reference)

### Creation Methods

[](#creation-methods)

- `Optional::empty()` - Creates an empty Optional
- `Optional::of($value)` - Creates Optional with non-null value (throws if null)
- `Optional::ofNullable($value)` - Creates Optional that may contain null

### Value Retrieval

[](#value-retrieval)

- `get()` - Returns value or throws `NoSuchElementException`
- `orElse($other)` - Returns value or provided default
- `orElseGet(Closure $supplier)` - Returns value or result of supplier
- `orElseThrow(Closure $exceptionSupplier)` - Returns value or throws exception
- `orNull()` - Returns value or null
- `or(Closure $supplier)` - Returns this Optional or alternative Optional

### Transformations

[](#transformations-1)

- `map(Closure $mapper)` - Transforms value if present
- `flatMap(Closure $mapper)` - Transforms value and flattens nested Optional

### Conditional Operations

[](#conditional-operations-1)

- `filter(Closure $predicate)` - Filters value by predicate
- `ifPresent(Closure $action)` - Executes action if value present
- `ifPresentOrElse(Closure $action, Closure $emptyAction)` - Executes action or alternative

### State Checking

[](#state-checking)

- `isPresent()` - Returns true if value exists
- `isEmpty()` - Returns true if no value

### Utilities

[](#utilities)

- `equals(Optional $other)` - Checks equality with another Optional
- `toString()` - Returns string representation
- `__toString()` - Magic method for string casting

Exception Handling
------------------

[](#exception-handling)

The library provides two custom exceptions:

### `NullPointerException`

[](#nullpointerexception)

Thrown when attempting to create an Optional with null using `of()`:

```
try {
    Optional::of(null); // Throws NullPointerException
} catch (NullPointerException $e) {
    echo $e->getMessage(); // "Value must not be null"
}
```

### `NoSuchElementException`

[](#nosuchelementexception)

Thrown when accessing value from empty Optional:

```
try {
    Optional::empty()->get(); // Throws NoSuchElementException
} catch (NoSuchElementException $e) {
    echo $e->getMessage(); // "No value present"
}
```

Both exceptions support custom messages:

```
throw new NullPointerException('Custom error message');
```

Type Safety
-----------

[](#type-safety)

This library provides comprehensive type safety through PHPStan and Psalm annotations:

```
/** @var Optional */
$userOptional = Optional::ofNullable($user);

/** @var Optional */
$nameOptional = $userOptional->map(fn(User $u) => $u->getName());
```

Best Practices
--------------

[](#best-practices)

1. **Use `ofNullable()` for potentially null values**

    ```
    // Good
    Optional::ofNullable($possiblyNull)

    // Bad - throws if null
    Optional::of($possiblyNull)
    ```
2. **Prefer `orElseGet()` over `orElse()` for expensive operations**

    ```
    // Good - lazy evaluation
    $value = $optional->orElseGet(fn() => expensiveCall());

    // Bad - always evaluated
    $value = $optional->orElse(expensiveCall());
    ```
3. **Chain operations for readability**

    ```
    $result = Optional::ofNullable($input)
        ->filter(fn($v) => strlen($v) > 0)
        ->map(fn($v) => trim($v))
        ->map(fn($v) => strtoupper($v))
        ->orElse('DEFAULT');
    ```
4. **Use `flatMap()` to avoid nested Optionals**

    ```
    // Good
    $email = Optional::of($user)
        ->flatMap(fn($u) => Optional::ofNullable($u->getEmail()))
        ->orElse(null);

    // Bad - returns Optional
    $email = Optional::of($user)
        ->map(fn($u) => Optional::ofNullable($u->getEmail()));
    ```

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

[](#development)

### Setup with Docker (Recommended)

[](#setup-with-docker-recommended)

```
# Initialize project
make init

# Start containers
make start

# Stop containers
make stop

# Rebuild containers
make rebuild

# Open PHP container shell
make php
```

### Setup with Composer

[](#setup-with-composer)

```
# Install dependencies
composer install

# Setup development tools
composer dev-tools-setup
```

### Testing

[](#testing)

```
# Run all tests with quality checks (Docker)
make test

# Run all tests with quality checks (Composer)
composer test

# Run only PHPUnit tests
composer phpunit

# Run static analysis
composer phpstan

# Fix code style
composer code-fix
```

### Available Make Commands

[](#available-make-commands)

```
# Show all available commands
make help

# Project lifecycle
make init              # Initialize the project
make rebuild           # Rebuild Docker containers
make start             # Start containers
make stop              # Stop containers
make down              # Remove containers

# Testing and quality
make test              # Run all tests
make code-fix          # Fix code style issues

# Composer operations
make composer-install  # Install dependencies
make composer-update   # Update dependencies

# PHP container access
make php               # Open PHP container shell
```

### Quality Tools

[](#quality-tools)

- **PHPUnit 13+** - Unit testing with 77 tests
- **PHPStan (max level)** - Static analysis
- **PHP CS Fixer** - Code style enforcement
- **Rector** - Code modernization
- **Psalm** - Additional type checking

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

[](#contributing)

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

License
-------

[](#license)

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

Credits
-------

[](#credits)

- **Author**: Anton Karpov ()
- Inspired by [Java's Optional class (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Optional.html)
- Built with modern PHP 8.4 features

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for version history.

Support
-------

[](#support)

For bugs, questions, and discussions please use the [GitHub Issues](https://github.com/awd-studio/vo-optional-php/issues).

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance85

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

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 ~29 days

Total

2

Last Release

59d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/15061745?v=4)[Anton Karpov](/maintainers/awd-studio)[@awd-studio](https://github.com/awd-studio)

---

Top Contributors

[![awd-studio](https://avatars.githubusercontent.com/u/15061745?v=4)](https://github.com/awd-studio "awd-studio (5 commits)")

---

Tags

null-safetyoptionaltype-safevalue-objectValue Objectimmutablephp8functional-programmingmonadmaybeoptionaltype-safenull-safetyjava-optional

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/awd-studio-vo-optional-php/health.svg)

```
[![Health](https://phpackages.com/badges/awd-studio-vo-optional-php/health.svg)](https://phpackages.com/packages/awd-studio-vo-optional-php)
```

###  Alternatives

[moneyphp/money

PHP implementation of Fowler's Money pattern

4.8k82.5M422](/packages/moneyphp-money)[aeon-php/calendar

PHP type safe, immutable calendar library

2079.7M16](/packages/aeon-php-calendar)[marcosh/lamphpda

A collection of functional programming data structures

12313.5k4](/packages/marcosh-lamphpda)[butschster/cron-expression-generator

Cron expression generator

511.4M2](/packages/butschster-cron-expression-generator)[andreas-glaser/php-helpers

A comprehensive collection of PHP utility functions for array manipulation, string operations, date handling, HTML generation, form building, validation, and more. Modern PHP 8.2+ library with full type safety.

1386.5k2](/packages/andreas-glaser-php-helpers)[fab2s/dt0

Immutable DTOs with bidirectional casting. No framework required. 8x faster than the alternative.

101.6k1](/packages/fab2s-dt0)

PHPackages © 2026

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