PHP Package Development
Introduction to PHP Package Development
PHP package development involves creating reusable code libraries that can be easily distributed and integrated into other projects. Modern PHP packages use Composer for dependency management and are typically distributed through Packagist, the main Composer repository.
What Makes a Good PHP Package?
Key Characteristics
- Single Responsibility: Focused on solving one specific problem
- Reusability: Can be used across different projects
- Well-Documented: Clear installation and usage instructions
- Tested: Comprehensive test suite
- Standards Compliant: Follows PSR standards
- Semantic Versioning: Uses proper versioning conventions
Package Structure
Basic Package Layout
my-package/
├── src/
│ ├── MyClass.php
│ └── Helper/
│ └── UtilityClass.php
├── tests/
│ ├── MyClassTest.php
│ └── Helper/
│ └── UtilityClassTest.php
├── docs/
│ └── README.md
├── .gitignore
├── .github/
│ └── workflows/
│ └── tests.yml
├── composer.json
├── phpunit.xml
├── LICENSE
└── README.md
composer.json Configuration
{
"name": "vendor/package-name",
"description": "A brief description of what your package does",
"type": "library",
"keywords": ["php", "library", "utility"],
"homepage": "https://github.com/vendor/package-name",
"license": "MIT",
"authors": [
{
"name": "Your Name",
"email": "[email protected]",
"homepage": "https://yourwebsite.com",
"role": "Developer"
}
],
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"phpstan/phpstan": "^1.0",
"squizlabs/php_codesniffer": "^3.6"
},
"autoload": {
"psr-4": {
"Vendor\\Package\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Vendor\\Package\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"test-coverage": "phpunit --coverage-html coverage",
"analyze": "phpstan analyse src --level max",
"cs-check": "phpcs src --standard=PSR12",
"cs-fix": "phpcbf src --standard=PSR12"
},
"config": {
"sort-packages": true
},
"minimum-stability": "stable",
"prefer-stable": true
}
Creating Your First Package
Step 1: Package Concept
Let's create a simple URL slug generator package:
<?php
// src/SlugGenerator.php
namespace Vendor\SlugGenerator;
class SlugGenerator
{
private $separator;
private $maxLength;
public function __construct(string $separator = '-', int $maxLength = 100)
{
$this->separator = $separator;
$this->maxLength = $maxLength;
}
public function generate(string $text): string
{
// Convert to lowercase
$slug = strtolower($text);
// Remove special characters and replace with separator
$slug = preg_replace('/[^a-z0-9]+/', $this->separator, $slug);
// Remove multiple separators
$slug = preg_replace('/[' . preg_quote($this->separator) . ']+/', $this->separator, $slug);
// Trim separators from ends
$slug = trim($slug, $this->separator);
// Truncate if necessary
if (strlen($slug) > $this->maxLength) {
$slug = substr($slug, 0, $this->maxLength);
$slug = rtrim($slug, $this->separator);
}
return $slug;
}
public function setSeparator(string $separator): self
{
$this->separator = $separator;
return $this;
}
public function setMaxLength(int $maxLength): self
{
$this->maxLength = $maxLength;
return $this;
}
}
?>
Step 2: Write Tests
<?php
// tests/SlugGeneratorTest.php
namespace Vendor\SlugGenerator\Tests;
use PHPUnit\Framework\TestCase;
use Vendor\SlugGenerator\SlugGenerator;
class SlugGeneratorTest extends TestCase
{
private $generator;
protected function setUp(): void
{
$this->generator = new SlugGenerator();
}
public function testBasicSlugGeneration()
{
$result = $this->generator->generate('Hello World');
$this->assertEquals('hello-world', $result);
}
public function testSpecialCharactersRemoval()
{
$result = $this->generator->generate('Hello, World! @#$%');
$this->assertEquals('hello-world', $result);
}
public function testCustomSeparator()
{
$generator = new SlugGenerator('_');
$result = $generator->generate('Hello World');
$this->assertEquals('hello_world', $result);
}
public function testMaxLengthTruncation()
{
$generator = new SlugGenerator('-', 10);
$result = $generator->generate('This is a very long title that should be truncated');
$this->assertEquals('this-is-a', $result);
}
public function testFluentInterface()
{
$result = $this->generator
->setSeparator('_')
->setMaxLength(15)
->generate('Hello Beautiful World');
$this->assertEquals('hello_beautiful', $result);
}
public function testEmptyString()
{
$result = $this->generator->generate('');
$this->assertEquals('', $result);
}
public function testMultipleSeparators()
{
$result = $this->generator->generate('Hello World Test');
$this->assertEquals('hello-world-test', $result);
}
}
?>
Step 3: Documentation
# Slug Generator
A simple PHP library for generating URL-friendly slugs from strings.
## Installation
```bash
composer require vendor/slug-generator
Usage
Basic Usage
use Vendor\SlugGenerator\SlugGenerator;
$generator = new SlugGenerator();
echo $generator->generate('Hello World!'); // outputs: hello-world
Custom Configuration
// Custom separator and max length
$generator = new SlugGenerator('_', 50);
echo $generator->generate('Hello World!'); // outputs: hello_world
// Fluent interface
$slug = $generator
->setSeparator('.')
->setMaxLength(20)
->generate('This is a test');
Requirements
- PHP 7.4 or higher
Testing
composer test
License
MIT License
## PSR Standards Compliance
### PSR-1: Basic Coding Standard
```php
<?php
// Use proper PHP tags
namespace Vendor\Package; // Use namespaces
class MyClass // Class names in StudlyCaps
{
const VERSION = '1.0.0'; // Constants in UPPER_CASE
public function myMethod() // Method names in camelCase
{
// Method implementation
}
}
?>
PSR-4: Autoloading Standard
{
"autoload": {
"psr-4": {
"Vendor\\Package\\": "src/",
"Vendor\\Package\\SubNamespace\\": "src/SubNamespace/"
}
}
}
PSR-12: Extended Coding Style
<?php
declare(strict_types=1);
namespace Vendor\Package;
use Another\Namespace\ClassName;
use Different\Namespace\AnotherClass;
class ExampleClass extends ParentClass implements InterfaceOne, InterfaceTwo
{
public const CONSTANT_VALUE = 'constant';
public function methodName(
string $firstArgument,
int $secondArgument = 0
): bool {
if ($firstArgument === 'test') {
return true;
}
return false;
}
}
?>
Dependency Management
Managing Dependencies
{
"require": {
"php": "^7.4 || ^8.0",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/console": "^5.0 || ^6.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"mockery/mockery": "^1.4"
}
}
Version Constraints
{
"require": {
"vendor/package": "1.0.0", // Exact version
"vendor/package": ">=1.0", // Greater than or equal
"vendor/package": "1.0.*", // Wildcard (1.0.0, 1.0.1, etc.)
"vendor/package": "~1.2", // Tilde (>=1.2.0, <2.0.0)
"vendor/package": "^1.2.3" // Caret (>=1.2.3, <2.0.0)
}
}
Package Types and Templates
Library Package
<?php
namespace Vendor\MathLibrary;
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;
}
}
?>
Service Provider Package (Laravel)
<?php
namespace Vendor\LaravelPackage;
use Illuminate\Support\ServiceProvider;
class MyPackageServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('my-service', function ($app) {
return new MyService($app['config']['my-package']);
});
}
public function boot()
{
// Publish config
$this->publishes([
__DIR__.'/../config/my-package.php' => config_path('my-package.php'),
], 'config');
// Load views
$this->loadViewsFrom(__DIR__.'/../resources/views', 'my-package');
// Load routes
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
}
?>
Console Application Package
<?php
namespace Vendor\ConsoleApp;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class GreetCommand extends Command
{
protected static $defaultName = 'app:greet';
protected function configure()
{
$this
->setDescription('Greets someone')
->setHelp('This command allows you to greet someone...');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Hello from my package!');
return Command::SUCCESS;
}
}
// composer.json bin configuration
{
"bin": ["bin/console"],
"scripts": {
"post-install-cmd": "chmod +x bin/console"
}
}
?>
Advanced Package Features
Configuration Files
<?php
// src/Config/Configuration.php
namespace Vendor\Package\Config;
class Configuration
{
private $options;
public function __construct(array $options = [])
{
$this->options = array_merge($this->getDefaults(), $options);
}
public function get(string $key, $default = null)
{
return $this->options[$key] ?? $default;
}
public function set(string $key, $value): void
{
$this->options[$key] = $value;
}
private function getDefaults(): array
{
return [
'timeout' => 30,
'retries' => 3,
'debug' => false,
];
}
}
?>
Plugin Architecture
<?php
namespace Vendor\Package\Plugin;
interface PluginInterface
{
public function getName(): string;
public function execute(array $data): array;
}
class PluginManager
{
private $plugins = [];
public function register(PluginInterface $plugin): void
{
$this->plugins[$plugin->getName()] = $plugin;
}
public function execute(string $pluginName, array $data): array
{
if (!isset($this->plugins[$pluginName])) {
throw new \InvalidArgumentException("Plugin '{$pluginName}' not found");
}
return $this->plugins[$pluginName]->execute($data);
}
public function getPlugins(): array
{
return array_keys($this->plugins);
}
}
// Example plugin
class TextTransformPlugin implements PluginInterface
{
public function getName(): string
{
return 'text-transform';
}
public function execute(array $data): array
{
$data['text'] = strtoupper($data['text'] ?? '');
return $data;
}
}
?>
Event System
<?php
namespace Vendor\Package\Event;
class EventDispatcher
{
private $listeners = [];
public function addEventListener(string $event, callable $listener): void
{
$this->listeners[$event][] = $listener;
}
public function dispatch(string $event, array $data = []): void
{
if (!isset($this->listeners[$event])) {
return;
}
foreach ($this->listeners[$event] as $listener) {
$listener($data);
}
}
}
class Event
{
private $name;
private $data;
private $stopped = false;
public function __construct(string $name, array $data = [])
{
$this->name = $name;
$this->data = $data;
}
public function getName(): string
{
return $this->name;
}
public function getData(): array
{
return $this->data;
}
public function stopPropagation(): void
{
$this->stopped = true;
}
public function isPropagationStopped(): bool
{
return $this->stopped;
}
}
?>
Quality Assurance
Code Style and Analysis
<!-- phpcs.xml -->
<?xml version="1.0"?>
<ruleset name="MyPackage">
<description>Coding standard for MyPackage</description>
<file>src</file>
<file>tests</file>
<rule ref="PSR12"/>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/build/*</exclude-pattern>
</ruleset>
# phpstan.neon
parameters:
level: max
paths:
- src
- tests
ignoreErrors:
- '#Call to an undefined method#'
excludePaths:
- vendor
Continuous Integration
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.4', '8.0', '8.1', '8.2']
dependency-version: [prefer-lowest, prefer-stable]
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl
coverage: xdebug
- name: Install dependencies
run: |
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Run tests
run: vendor/bin/phpunit --coverage-clover coverage.xml
- name: Run PHPStan
run: vendor/bin/phpstan analyse
- name: Check code style
run: vendor/bin/phpcs
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
Semantic Versioning
Version Format
MAJOR.MINOR.PATCH
- MAJOR: Incompatible API changes
- MINOR: Backward-compatible functionality additions
- PATCH: Backward-compatible bug fixes
Examples
{
"version": "1.0.0", // Initial release
"version": "1.0.1", // Bug fix
"version": "1.1.0", // New feature (backward compatible)
"version": "2.0.0" // Breaking change
}
Git Tagging
# Create and push a new version tag
git tag -a v1.2.0 -m "Version 1.2.0"
git push origin v1.2.0
# List all tags
git tag -l
# Delete a tag
git tag -d v1.2.0
git push origin --delete v1.2.0
Package Distribution
Publishing to Packagist
- Create Packagist Account: Sign up at packagist.org
- Submit Package: Add your GitHub repository URL
- Set up Auto-updating: Configure webhook for automatic updates
- Maintain Package: Keep versions updated and respond to issues
Private Repositories
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/company/private-package"
}
],
"require": {
"company/private-package": "^1.0"
}
}
Satis (Private Packagist)
{
"name": "My Company",
"homepage": "https://packages.mycompany.com",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/mycompany/package1"
},
{
"type": "vcs",
"url": "https://github.com/mycompany/package2"
}
],
"require-all": true
}
Best Practices Summary
- Follow PSR Standards: Use PSR-1, PSR-4, and PSR-12
- Write Tests: Aim for high code coverage
- Document Everything: README, API docs, examples
- Use Semantic Versioning: Proper version management
- Automate Quality Checks: CI/CD, static analysis
- Keep Dependencies Minimal: Only include what's necessary
- Support Multiple PHP Versions: Test against different versions
- Provide Migration Guides: For breaking changes
- Respond to Issues: Maintain community engagement
- Security First: Regular updates and security patches
Creating successful PHP packages requires attention to code quality, documentation, testing, and community engagement. Start small, follow standards, and gradually build more complex packages as you gain experience.