1. php
  2. /packages
  3. /creating-packages

Creating PHP Packages

Introduction to PHP Package Development

Creating reusable PHP packages allows you to share functionality across projects and contribute to the open-source community. Modern PHP packages follow PSR standards and use Composer for dependency management.

Why Create PHP Packages?

Code Reusability: Instead of copying code between projects, packages allow you to maintain a single source of truth. Updates and bug fixes benefit all projects using the package.

Community Contribution: Open-source packages help other developers solve common problems and contribute to the PHP ecosystem's growth.

Professional Development: Creating packages demonstrates expertise, improves code quality through peer review, and builds your professional reputation.

Modular Architecture: Packages enforce separation of concerns and clean architecture, making applications more maintainable and testable.

Package Structure

Basic Package Structure

my-package/
├── src/
│   ├── Calculator.php
│   └── Math/
│       ├── Operations.php
│       └── Advanced.php
├── tests/
│   ├── CalculatorTest.php
│   └── Math/
│       ├── OperationsTest.php
│       └── AdvancedTest.php
├── docs/
│   └── usage.md
├── composer.json
├── README.md
├── LICENSE
├── CHANGELOG.md
├── phpunit.xml
└── .gitignore

Directory Structure Explained:

src/: Contains all package source code. This is the heart of your package where the actual functionality lives. The directory structure should mirror your namespace structure for PSR-4 autoloading compliance.

tests/: Houses all test files, typically mirroring the src/ structure. This parallel structure makes it easy to locate tests for specific classes and ensures comprehensive test coverage.

docs/: Optional documentation directory for detailed guides, API references, and examples beyond what fits in the README.

Root Files:

  • composer.json: Package manifest defining dependencies, autoloading, and metadata
  • README.md: First point of contact for users, containing installation and basic usage
  • LICENSE: Legal terms under which the package is distributed
  • CHANGELOG.md: Version history documenting changes, fixes, and new features
  • phpunit.xml: PHPUnit configuration for consistent test execution
  • .gitignore: Prevents unnecessary files from version control

Advanced Package Structure

enterprise-package/
├── bin/
│   └── console
├── config/
│   ├── services.yml
│   └── routes.yml
├── resources/
│   ├── views/
│   │   └── templates/
│   └── assets/
│       ├── css/
│       └── js/
├── src/
│   ├── Commands/
│   │   └── ProcessCommand.php
│   ├── Controllers/
│   │   └── ApiController.php
│   ├── Models/
│   │   └── User.php
│   ├── Services/
│   │   └── EmailService.php
│   └── Providers/
│       └── ServiceProvider.php
├── tests/
│   ├── Unit/
│   ├── Integration/
│   └── Feature/
├── database/
│   ├── migrations/
│   └── seeds/
├── composer.json
├── phpunit.xml
├── .github/
│   └── workflows/
│       └── tests.yml
└── docker-compose.yml

Advanced Structure Components:

bin/: Executable scripts that users can run from command line. Composer can symlink these to vendor/bin for easy access.

config/: Configuration files for services, routes, or other package settings. Separating configuration from code improves flexibility.

resources/: Non-PHP assets like templates, stylesheets, or JavaScript files. Essential for packages providing UI components.

Organized src/:

  • Commands/: CLI commands for packages integrating with console applications
  • Controllers/: HTTP controllers for packages providing web functionality
  • Models/: Data models representing business entities
  • Services/: Business logic and external service integrations
  • Providers/: Service providers for dependency injection containers

Test Organization:

  • Unit/: Isolated tests for individual classes
  • Integration/: Tests verifying component interactions
  • Feature/: End-to-end tests of complete features

Infrastructure:

  • .github/workflows/: GitHub Actions for automated testing and deployment
  • docker-compose.yml: Containerized development environment

Composer Configuration

Basic composer.json

{
    "name": "vendor/package-name",
    "description": "A brief description of your package",
    "type": "library",
    "license": "MIT",
    "keywords": ["php", "library", "utility"],
    "homepage": "https://github.com/vendor/package-name",
    "authors": [
        {
            "name": "Your Name",
            "email": "[email protected]",
            "homepage": "https://yourwebsite.com",
            "role": "Developer"
        }
    ],
    "require": {
        "php": "^8.0",
        "psr/log": "^1.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.0",
        "squizlabs/php_codesniffer": "^3.5",
        "phpstan/phpstan": "^0.12"
    },
    "autoload": {
        "psr-4": {
            "Vendor\\PackageName\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Vendor\\PackageName\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "cs-check": "phpcs src tests --standard=PSR12",
        "cs-fix": "phpcbf src tests --standard=PSR12",
        "analyze": "phpstan analyse src --level=8"
    },
    "config": {
        "sort-packages": true
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}

Composer.json Breakdown:

Package Identification:

  • name: Unique identifier in vendor/package format. Choose names that are descriptive and avoid conflicts
  • description: Concise explanation helping users understand the package's purpose
  • type: Usually "library" for reusable packages, but can be "project", "metapackage", etc.
  • license: Legal license (MIT, GPL, Apache, etc.). Critical for open-source adoption

Metadata:

  • keywords: Help users discover your package through search
  • homepage: Project website or repository URL
  • authors: Credit contributors and provide contact information

Dependencies:

  • require: Production dependencies needed for the package to function
  • require-dev: Development-only dependencies for testing and code quality
  • Version constraints: Use semantic versioning (^, ~, >=) to balance stability and updates

Autoloading:

  • PSR-4: Modern autoloading standard mapping namespaces to directories
  • autoload-dev: Separate autoloading for test classes, keeping production lean

Scripts:

  • Shortcuts for common tasks
  • Ensure consistent command usage across contributors
  • Can chain multiple commands or run custom PHP scripts

Advanced composer.json

{
    "name": "vendor/advanced-package",
    "description": "An advanced PHP package with additional features",
    "type": "library",
    "license": "MIT",
    "keywords": ["php", "framework", "api", "oauth"],
    "homepage": "https://github.com/vendor/advanced-package",
    "support": {
        "issues": "https://github.com/vendor/advanced-package/issues",
        "wiki": "https://github.com/vendor/advanced-package/wiki",
        "docs": "https://docs.example.com"
    },
    "authors": [
        {
            "name": "Your Name",
            "email": "[email protected]",
            "role": "Lead Developer"
        }
    ],
    "require": {
        "php": "^8.0",
        "guzzlehttp/guzzle": "^7.0",
        "monolog/monolog": "^2.0",
        "symfony/console": "^5.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.0",
        "mockery/mockery": "^1.4",
        "squizlabs/php_codesniffer": "^3.5",
        "phpstan/phpstan": "^0.12",
        "psalm/phar": "^4.0"
    },
    "suggest": {
        "ext-curl": "Required for HTTP client functionality",
        "ext-json": "Required for JSON processing",
        "vendor/optional-package": "Adds additional features"
    },
    "autoload": {
        "psr-4": {
            "Vendor\\AdvancedPackage\\": "src/"
        },
        "files": [
            "src/helpers.php"
        ]
    },
    "autoload-dev": {
        "psr-4": {
            "Vendor\\AdvancedPackage\\Tests\\": "tests/"
        }
    },
    "bin": [
        "bin/console"
    ],
    "scripts": {
        "test": "phpunit",
        "test-coverage": "phpunit --coverage-html coverage",
        "cs-check": "phpcs",
        "cs-fix": "phpcbf",
        "analyze": [
            "phpstan analyse",
            "psalm"
        ],
        "post-install-cmd": [
            "@php bin/console cache:clear"
        ],
        "post-update-cmd": [
            "@php bin/console cache:clear"
        ]
    },
    "extra": {
        "branch-alias": {
            "dev-master": "1.0-dev"
        }
    },
    "config": {
        "sort-packages": true,
        "optimize-autoloader": true,
        "classmap-authoritative": true
    }
}

Advanced Configuration Features:

Support Information:

  • issues: Direct users to report bugs
  • wiki: Community-maintained documentation
  • docs: Official documentation site
  • Improves user experience and reduces support burden

Suggestions:

  • Optional dependencies that enhance functionality
  • PHP extensions that improve performance
  • Related packages that work well together
  • Helps users optimize their setup

Binary Files:

  • Executable scripts installed to vendor/bin
  • Makes CLI tools easily accessible
  • Enables composer exec usage

Advanced Scripts:

  • test-coverage: Generates code coverage reports
  • analyze: Runs multiple static analysis tools
  • post-install/update-cmd: Automatic setup tasks
  • Arrays allow multiple commands in sequence

Extra Configuration:

  • branch-alias: Version aliases for development branches
  • Optimization flags: Improve autoloader performance
  • Custom package-specific configuration

Namespacing and Autoloading

PSR-4 Autoloading

<?php
// src/Calculator.php
namespace Vendor\PackageName;

class Calculator
{
    public function add(float $a, float $b): float
    {
        return $a + $b;
    }
    
    public function subtract(float $a, float $b): float
    {
        return $a - $b;
    }
    
    public function multiply(float $a, float $b): float
    {
        return $a * $b;
    }
    
    public function divide(float $a, float $b): float
    {
        if ($b === 0.0) {
            throw new \InvalidArgumentException('Division by zero');
        }
        
        return $a / $b;
    }
}
?>

PSR-4 Autoloading Principles:

Namespace Structure:

  • Matches directory structure exactly
  • Vendor name prevents conflicts with other packages
  • Package name groups related functionality
  • Follows PSR-4 standard for interoperability

Class Design:

  • Single responsibility: Calculator handles basic arithmetic
  • Type declarations ensure type safety
  • Exceptions for error conditions
  • Clear method names describe operations

File Organization:

  • One class per file
  • Filename matches class name exactly
  • Directory path matches namespace hierarchy
  • Enables automatic class loading

Nested Namespaces

<?php
// src/Math/Operations.php
namespace Vendor\PackageName\Math;

class Operations
{
    public static function factorial(int $n): int
    {
        if ($n < 0) {
            throw new \InvalidArgumentException('Factorial is not defined for negative numbers');
        }
        
        if ($n === 0 || $n === 1) {
            return 1;
        }
        
        return $n * self::factorial($n - 1);
    }
    
    public static function fibonacci(int $n): int
    {
        if ($n < 0) {
            throw new \InvalidArgumentException('Fibonacci is not defined for negative numbers');
        }
        
        if ($n === 0) return 0;
        if ($n === 1) return 1;
        
        return self::fibonacci($n - 1) + self::fibonacci($n - 2);
    }
}

// src/Math/Advanced.php
namespace Vendor\PackageName\Math;

class Advanced
{
    public static function power(float $base, float $exponent): float
    {
        return pow($base, $exponent);
    }
    
    public static function sqrt(float $number): float
    {
        if ($number < 0) {
            throw new \InvalidArgumentException('Square root of negative number');
        }
        
        return sqrt($number);
    }
    
    public static function logarithm(float $number, float $base = M_E): float
    {
        if ($number <= 0 || $base <= 0 || $base === 1) {
            throw new \InvalidArgumentException('Invalid logarithm arguments');
        }
        
        return log($number) / log($base);
    }
}
?>

Namespace Organization Benefits:

Logical Grouping:

  • Related classes share namespace
  • Clear hierarchy indicates relationships
  • Easier to locate functionality
  • Supports future expansion

Static Methods Consideration:

  • Used for stateless operations
  • No need for instantiation
  • Similar to namespaced functions
  • Consider regular classes for stateful operations

Error Handling:

  • Validate inputs before processing
  • Throw meaningful exceptions
  • Use appropriate exception types
  • Document error conditions

Helper Functions

<?php
// src/helpers.php
if (!function_exists('calculate')) {
    function calculate(string $operation, float $a, float $b = null): float
    {
        $calculator = new \Vendor\PackageName\Calculator();
        
        switch ($operation) {
            case 'add':
                return $calculator->add($a, $b);
            case 'subtract':
                return $calculator->subtract($a, $b);
            case 'multiply':
                return $calculator->multiply($a, $b);
            case 'divide':
                return $calculator->divide($a, $b);
            default:
                throw new \InvalidArgumentException("Unknown operation: {$operation}");
        }
    }
}

if (!function_exists('factorial')) {
    function factorial(int $n): int
    {
        return \Vendor\PackageName\Math\Operations::factorial($n);
    }
}

if (!function_exists('fibonacci')) {
    function fibonacci(int $n): int
    {
        return \Vendor\PackageName\Math\Operations::fibonacci($n);
    }
}
?>

Helper Functions Strategy:

Function Guard:

  • function_exists() prevents redefinition errors
  • Allows packages to coexist peacefully
  • Users can override if needed
  • Standard practice for global functions

Convenience Layer:

  • Simplifies common operations
  • Provides procedural interface to OOP code
  • Reduces verbosity for frequent tasks
  • Optional - users can still use classes directly

Namespace Considerations:

  • Global functions don't use namespaces
  • Risk of naming conflicts
  • Document available helpers clearly
  • Consider prefixing function names

Configuration and Service Providers

Configuration Class

<?php
// src/Config/Configuration.php
namespace Vendor\PackageName\Config;

class Configuration
{
    private array $config;
    
    public function __construct(array $config = [])
    {
        $this->config = array_merge($this->getDefaults(), $config);
    }
    
    public function get(string $key, $default = null)
    {
        return $this->config[$key] ?? $default;
    }
    
    public function set(string $key, $value): void
    {
        $this->config[$key] = $value;
    }
    
    public function has(string $key): bool
    {
        return isset($this->config[$key]);
    }
    
    public function all(): array
    {
        return $this->config;
    }
    
    private function getDefaults(): array
    {
        return [
            'precision' => 2,
            'rounding_mode' => PHP_ROUND_HALF_UP,
            'cache_enabled' => true,
            'cache_ttl' => 3600,
            'debug' => false
        ];
    }
}
?>

Configuration Management Explained:

Design Pattern:

  • Centralized configuration management
  • Default values with override capability
  • Type-agnostic storage
  • Simple getter/setter interface

Default Values:

  • Sensible defaults reduce setup friction
  • Document what each option controls
  • Consider environment-specific defaults
  • Make defaults production-safe

Configuration Methods:

  • get(): Retrieve with optional default
  • set(): Runtime configuration changes
  • has(): Check existence before access
  • all(): Debugging and serialization

Service Provider

<?php
// src/Providers/CalculatorServiceProvider.php
namespace Vendor\PackageName\Providers;

use Vendor\PackageName\Calculator;
use Vendor\PackageName\Config\Configuration;
use Vendor\PackageName\Services\CacheService;

class CalculatorServiceProvider
{
    private Configuration $config;
    
    public function __construct(Configuration $config)
    {
        $this->config = $config;
    }
    
    public function register(): array
    {
        return [
            Calculator::class => function() {
                return new Calculator($this->config);
            },
            CacheService::class => function() {
                return new CacheService(
                    $this->config->get('cache_enabled'),
                    $this->config->get('cache_ttl')
                );
            }
        ];
    }
    
    public function boot(): void
    {
        // Perform any initialization logic
        if ($this->config->get('debug')) {
            error_reporting(E_ALL);
        }
    }
}
?>

Service Provider Pattern:

Dependency Injection:

  • Centralizes service creation
  • Manages dependencies between services
  • Enables lazy instantiation
  • Supports different implementations

Registration Phase:

  • Returns service factories
  • Services created on demand
  • Closures capture configuration
  • Enables circular dependency resolution

Boot Phase:

  • Runs after all services registered
  • Performs initialization tasks
  • Sets up global configuration
  • Registers event listeners

Exception Handling

Custom Exceptions

<?php
// src/Exceptions/CalculatorException.php
namespace Vendor\PackageName\Exceptions;

class CalculatorException extends \Exception
{
    public static function divisionByZero(): self
    {
        return new self('Division by zero is not allowed');
    }
    
    public static function invalidOperation(string $operation): self
    {
        return new self("Invalid operation: {$operation}");
    }
    
    public static function invalidArgument(string $message): self
    {
        return new self("Invalid argument: {$message}");
    }
}

// src/Exceptions/MathException.php
namespace Vendor\PackageName\Exceptions;

class MathException extends CalculatorException
{
    public static function negativeFactorial(): self
    {
        return new self('Factorial is not defined for negative numbers');
    }
    
    public static function invalidLogarithm(): self
    {
        return new self('Invalid logarithm arguments');
    }
}
?>

Custom Exception Benefits:

Named Constructors:

  • Static factory methods provide clarity
  • Consistent error messages
  • Easy to refactor messages
  • Type-safe exception creation

Exception Hierarchy:

  • Base exception for package-wide catching
  • Specialized exceptions for specific errors
  • Allows fine-grained error handling
  • Maintains backwards compatibility

Usage Benefits:

  • Self-documenting code
  • Centralized error messages
  • Easier testing
  • Better IDE support

Interface Design

Contracts and Interfaces

<?php
// src/Contracts/CalculatorInterface.php
namespace Vendor\PackageName\Contracts;

interface CalculatorInterface
{
    public function add(float $a, float $b): float;
    public function subtract(float $a, float $b): float;
    public function multiply(float $a, float $b): float;
    public function divide(float $a, float $b): float;
}

// src/Contracts/CacheInterface.php
namespace Vendor\PackageName\Contracts;

interface CacheInterface
{
    public function get(string $key);
    public function set(string $key, $value, int $ttl = null): bool;
    public function delete(string $key): bool;
    public function clear(): bool;
}

// src/Contracts/LoggerInterface.php
namespace Vendor\PackageName\Contracts;

interface LoggerInterface
{
    public function log(string $level, string $message, array $context = []): void;
    public function debug(string $message, array $context = []): void;
    public function info(string $message, array $context = []): void;
    public function warning(string $message, array $context = []): void;
    public function error(string $message, array $context = []): void;
}
?>

Implementation

<?php
// src/Services/CacheService.php
namespace Vendor\PackageName\Services;

use Vendor\PackageName\Contracts\CacheInterface;

class CacheService implements CacheInterface
{
    private bool $enabled;
    private int $defaultTtl;
    private array $cache = [];
    
    public function __construct(bool $enabled = true, int $defaultTtl = 3600)
    {
        $this->enabled = $enabled;
        $this->defaultTtl = $defaultTtl;
    }
    
    public function get(string $key)
    {
        if (!$this->enabled) {
            return null;
        }
        
        if (!isset($this->cache[$key])) {
            return null;
        }
        
        $item = $this->cache[$key];
        
        if ($item['expires'] < time()) {
            unset($this->cache[$key]);
            return null;
        }
        
        return $item['value'];
    }
    
    public function set(string $key, $value, int $ttl = null): bool
    {
        if (!$this->enabled) {
            return false;
        }
        
        $ttl = $ttl ?: $this->defaultTtl;
        
        $this->cache[$key] = [
            'value' => $value,
            'expires' => time() + $ttl
        ];
        
        return true;
    }
    
    public function delete(string $key): bool
    {
        if (isset($this->cache[$key])) {
            unset($this->cache[$key]);
            return true;
        }
        
        return false;
    }
    
    public function clear(): bool
    {
        $this->cache = [];
        return true;
    }
}
?>

Testing Your Package

Unit Tests

<?php
// tests/CalculatorTest.php
namespace Vendor\PackageName\Tests;

use PHPUnit\Framework\TestCase;
use Vendor\PackageName\Calculator;
use Vendor\PackageName\Config\Configuration;
use Vendor\PackageName\Exceptions\CalculatorException;

class CalculatorTest extends TestCase
{
    private Calculator $calculator;
    
    protected function setUp(): void
    {
        $this->calculator = new Calculator();
    }
    
    public function testAddition(): void
    {
        $result = $this->calculator->add(2, 3);
        $this->assertEquals(5, $result);
    }
    
    public function testDivision(): void
    {
        $result = $this->calculator->divide(10, 2);
        $this->assertEquals(5, $result);
    }
    
    public function testDivisionByZero(): void
    {
        $this->expectException(CalculatorException::class);
        $this->expectExceptionMessage('Division by zero is not allowed');
        
        $this->calculator->divide(10, 0);
    }
    
    public function testConfigurationPrecision(): void
    {
        $config = new Configuration(['precision' => 3]);
        $calculator = new Calculator($config);
        
        $result = $calculator->divide(10, 3);
        $this->assertEquals(3.333, $result);
    }
    
    /**
     * @dataProvider arithmeticOperationsProvider
     */
    public function testArithmeticOperations(
        string $operation,
        float $a,
        float $b,
        float $expected
    ): void {
        $result = $this->calculator->$operation($a, $b);
        $this->assertEquals($expected, $result);
    }
    
    public function arithmeticOperationsProvider(): array
    {
        return [
            'addition' => ['add', 2, 3, 5],
            'subtraction' => ['subtract', 5, 3, 2],
            'multiplication' => ['multiply', 4, 3, 12],
            'division' => ['divide', 8, 2, 4],
        ];
    }
}
?>

Integration Tests

<?php
// tests/Integration/FullCalculatorTest.php
namespace Vendor\PackageName\Tests\Integration;

use PHPUnit\Framework\TestCase;
use Vendor\PackageName\Calculator;
use Vendor\PackageName\Config\Configuration;
use Vendor\PackageName\Services\CacheService;
use Vendor\PackageName\Math\Operations;

class FullCalculatorTest extends TestCase
{
    public function testCompleteCalculationWorkflow(): void
    {
        // Setup configuration
        $config = new Configuration([
            'precision' => 2,
            'cache_enabled' => true
        ]);
        
        // Initialize services
        $calculator = new Calculator($config);
        $cache = new CacheService($config->get('cache_enabled'));
        
        // Perform calculations
        $result1 = $calculator->add(10.555, 5.444);
        $this->assertEquals(16.00, $result1);
        
        // Cache the result
        $cache->set('last_result', $result1);
        
        // Use cached result
        $cachedResult = $cache->get('last_result');
        $this->assertEquals($result1, $cachedResult);
        
        // Perform complex operation
        $factorial = Operations::factorial(5);
        $finalResult = $calculator->divide($factorial, $cachedResult);
        
        $this->assertEquals(7.50, $finalResult);
    }
}
?>

Package Documentation

README.md Example

# Package Name

A brief description of what your package does.

[![Latest Version on Packagist](https://img.shields.io/packagist/v/vendor/package-name.svg?style=flat-square)](https://packagist.org/packages/vendor/package-name)
[![Total Downloads](https://img.shields.io/packagist/dt/vendor/package-name.svg?style=flat-square)](https://packagist.org/packages/vendor/package-name)
[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/vendor/package-name/run-tests?label=tests)](https://github.com/vendor/package-name/actions?query=workflow%3Arun-tests+branch%3Amain)

## Installation

You can install the package via composer:

```bash
composer require vendor/package-name

Usage

use Vendor\PackageName\Calculator;

$calculator = new Calculator();
$result = $calculator->add(2, 3);
echo $result; // 5

Advanced Usage

use Vendor\PackageName\Calculator;
use Vendor\PackageName\Config\Configuration;

$config = new Configuration(['precision' => 3]);
$calculator = new Calculator($config);

$result = $calculator->divide(10, 3);
echo $result; // 3.333

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.


## PHPUnit Configuration

```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory suffix="Test.php">./tests/Integration</directory>
        </testsuite>
    </testsuites>
    
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./src</directory>
        </include>
        <exclude>
            <directory suffix=".php">./src/Config</directory>
            <file>./src/helpers.php</file>
        </exclude>
    </coverage>
    
    <logging>
        <junit outputFile="build/report.junit.xml"/>
        <teamcity outputFile="build/report.teamcity.txt"/>
        <testdoxHtml outputFile="build/testdox.html"/>
        <testdoxText outputFile="build/testdox.txt"/>
    </logging>
</phpunit>

Creating well-structured, tested, and documented PHP packages enables code reuse, maintainability, and contribution to the broader PHP ecosystem.