1. php
  2. /packages

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

  1. Single Responsibility: Focused on solving one specific problem
  2. Reusability: Can be used across different projects
  3. Well-Documented: Clear installation and usage instructions
  4. Tested: Comprehensive test suite
  5. Standards Compliant: Follows PSR standards
  6. 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

  1. Create Packagist Account: Sign up at packagist.org
  2. Submit Package: Add your GitHub repository URL
  3. Set up Auto-updating: Configure webhook for automatic updates
  4. 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

  1. Follow PSR Standards: Use PSR-1, PSR-4, and PSR-12
  2. Write Tests: Aim for high code coverage
  3. Document Everything: README, API docs, examples
  4. Use Semantic Versioning: Proper version management
  5. Automate Quality Checks: CI/CD, static analysis
  6. Keep Dependencies Minimal: Only include what's necessary
  7. Support Multiple PHP Versions: Test against different versions
  8. Provide Migration Guides: For breaking changes
  9. Respond to Issues: Maintain community engagement
  10. 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.