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.
[](https://packagist.org/packages/vendor/package-name)
[](https://packagist.org/packages/vendor/package-name)
[](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.