PHP Package Development with Composer: Build & Publish
Learn to create, publish, and maintain PHP packages with Composer.
20+ years shipping production PHP systems at scale. Written from production experience, not tutorials.
- ✓Basic knowledge of PHP and object-oriented programming
- ✓Familiarity with Composer (installing and using packages)
- ✓A local PHP development environment (PHP 8.0+)
- ✓Git and a GitHub account (for publishing)
- Composer is the dependency manager for PHP; packages are reusable code libraries.
- A package requires a composer.json file with name, autoload, and require sections.
- Use PSR-4 autoloading to map namespaces to directories.
- Publish packages on Packagist to make them installable via Composer.
- Follow semantic versioning and include a README, license, and tests.
Think of Composer like an app store for PHP code. Instead of copying and pasting code from one project to another, you create a 'package' (like an app) that can be installed with a single command. Just as an app store handles updates and dependencies, Composer manages versions and required libraries for you.
In modern PHP development, reusing code across projects is essential for efficiency and maintainability. Composer, the de facto dependency manager for PHP, allows you to create packages that can be easily shared and installed. Whether you're building a utility library, a framework extension, or an SDK for an API, understanding package development is a key skill for any advanced PHP developer.
This tutorial will guide you through the entire process: from setting up your package structure, writing robust code with proper autoloading, to publishing on Packagist. You'll learn how to handle dependencies, versioning, and testing. We'll also cover real-world debugging scenarios and common pitfalls.
By the end, you'll be able to create a production-ready PHP package that others can install with composer require your-vendor/your-package. Let's dive in!
Setting Up Your Package Structure
A well-organized package structure is crucial for maintainability and autoloading. Start by creating a directory for your package. Typically, the structure looks like this:
`` my-package/ ├── src/ │ └── MyClass.php ├── tests/ │ └── MyClassTest.php ├── composer.json ├── README.md ├── LICENSE └── .gitignore ``
The src/ directory contains your source code. The tests/ directory holds PHPUnit tests. The composer.json file is the heart of your package, defining metadata, autoloading, and dependencies.
Create a composer.json file with the following minimal configuration:
``json { "name": "your-vendor/your-package", "description": "A brief description of your package", "type": "library", "autoload": { "psr-4": { "YourVendor\\YourPackage\\": "src/" } }, "require": { "php": ">=8.0" }, "require-dev": { "phpunit/phpunit": "^9.5" }, "license": "MIT", "authors": [ { "name": "Your Name", "email": "your@email.com" } ] } ``
Replace your-vendor/your-package with your actual vendor and package name. The autoload section uses PSR-4 to map the namespace YourVendor\YourPackage\ to the src/ directory. This allows Composer to automatically load your classes.
composer validate to check your composer.json for syntax errors before committing.Writing Your First Package Class
Now, let's create a simple class inside the src/ directory. Create src/MyClass.php with the following code:
```php <?php
namespace YourVendor\YourPackage;
class MyClass { public function greet(string $name): string { return "Hello, $name!"; } } ```
Notice the namespace matches the PSR-4 mapping in composer.json. The class is simple but demonstrates the structure. To test it locally, you can create an index.php file in the package root (but not included in the package) or use a test script.
To use the package in another project, you would require it via Composer. But first, let's set up local development. In your package directory, run:
``bash composer install ``
This installs development dependencies (like PHPUnit) and generates the autoloader. You can now write a quick test script:
```php <?php require 'vendor/autoload.php';
use YourVendor\YourPackage\MyClass;
$obj = new MyClass(); echo $obj->greet('World'); ```
Run it with php test.php and you should see "Hello, World!".
Managing Dependencies
Your package may depend on other packages. For example, if your package needs to send HTTP requests, you might depend on Guzzle. Add dependencies to the require section of composer.json:
``json "require": { "php": ">=8.0", "guzzlehttp/guzzle": "^7.0" } ``
After updating composer.json, run composer update to install the dependency. Composer will also update the composer.lock file. However, for a package, you should not commit the composer.lock file because it's meant for applications, not libraries. Add composer.lock to your .gitignore.
When someone installs your package, Composer will resolve the dependencies based on the version constraints you specified. Use semantic versioning: ^7.0 means any version from 7.0.0 up to 8.0.0 (exclusive).
For development-only dependencies (like PHPUnit), use require-dev. These are not installed when someone requires your package as a dependency.
Always specify the minimum PHP version your package supports. This helps users avoid compatibility issues.
Testing Your Package
Testing is crucial for package reliability. PHPUnit is the standard testing framework. Create a test file in the tests/ directory:
```php <?php
namespace YourVendor\YourPackage\Tests;
use PHPUnit\Framework\TestCase; use YourVendor\YourPackage\MyClass;
class MyClassTest extends TestCase { public function testGreet() { $obj = new MyClass(); $this->assertEquals('Hello, World!', $obj->greet('World')); } } ```
Add the following to your composer.json to autoload test files:
``json "autoload-dev": { "psr-4": { "YourVendor\\YourPackage\\Tests\\": "tests/" } } ``
Then run tests with:
``bash ./vendor/bin/phpunit ``
You should see green output. Always write tests for your public API. Consider edge cases and error conditions.
For continuous integration, you can use GitHub Actions or GitLab CI to run tests automatically on push.
Publishing Your Package on Packagist
Packagist is the main repository for Composer packages. To publish, you need a GitHub (or GitLab/Bitbucket) repository and a Packagist account.
- Push your package to a public GitHub repository.
- Go to [packagist.org](https://packagist.org) and log in with your GitHub account.
- Click "Submit" and enter the URL of your GitHub repository.
- Packagist will detect the composer.json and create the package.
Once published, anyone can install your package with:
``bash composer require your-vendor/your-package ``
To update your package, push a new tag to GitHub (e.g., v1.0.1). Packagist will automatically update via a webhook if you configured it, or you can manually update.
Important: Your package name must be unique on Packagist. Use a vendor name that you control (e.g., your GitHub username).
Best Practices and Security
When developing packages, follow these best practices:
- Use strict types: Declare
declare(strict_types=1);in all PHP files to enforce type safety. - Validate inputs: Always validate and sanitize inputs, especially if your package deals with user data.
- Avoid hardcoded secrets: Never include API keys or passwords in your package. Use environment variables or configuration files.
- Keep dependencies minimal: Only require what you need. Fewer dependencies reduce security risks and conflicts.
- Use a license: Choose an open-source license like MIT or Apache 2.0. Include a LICENSE file.
- Write documentation: A good README explains installation, usage, and examples.
Security is paramount. If your package handles sensitive data, consider using encryption. Regularly update dependencies to patch vulnerabilities. Use tools like composer audit to check for known security issues.
Example of a secure class:
```php <?php
declare(strict_types=1);
namespace YourVendor\YourPackage;
class SecureStorage { private string $encryptionKey;
public function __construct(string $encryptionKey) { if (strlen($encryptionKey) < 32) { throw new \InvalidArgumentException('Encryption key must be at least 32 characters.'); } $this->encryptionKey = $encryptionKey; }
public function encrypt(string $data): string { $iv = random_bytes(16); $encrypted = openssl_encrypt($data, 'aes-256-cbc', $this->encryptionKey, 0, $iv); return base64_encode($iv . $encrypted); } } ```
composer audit regularly to check for vulnerabilities in your dependencies.The Case of the Missing Autoloader
- Always verify autoloading configuration with 'composer dump-autoload -o' after changes.
- Use 'composer validate' to check your composer.json for errors.
- Test package installation in a fresh project before deployment.
- Ensure the namespace in composer.json matches the actual namespace in PHP files.
- Include a 'vendor/autoload.php' require in your entry point.
composer dump-autoload -ocomposer validate| File | Command / Code | Purpose |
|---|---|---|
| composer.json | { | Setting Up Your Package Structure |
| src | namespace YourVendor\YourPackage; | Writing Your First Package Class |
| composer.json (with dependencies) | { | Managing Dependencies |
| tests | namespace YourVendor\YourPackage\Tests; | Testing Your Package |
| Git tag command | git tag v1.0.0 | Publishing Your Package on Packagist |
| src | declare(strict_types=1); | Best Practices and Security |
Key takeaways
Common mistakes to avoid
3 patternsForgetting to add the `autoload` section in composer.json.
Committing the `composer.lock` file in a package repository.
Using exact version constraints like `"1.0.0"` for dependencies.
Interview Questions on This Topic
Explain PSR-4 autoloading and how it works with Composer.
YourVendor\YourPackage\MyClass maps to src/MyClass.php if the prefix is YourVendor\YourPackage\ and the base directory is src/.Frequently Asked Questions
20+ years shipping production PHP systems at scale. Written from production experience, not tutorials.
That's Advanced PHP. Mark it forged?
4 min read · try the examples if you haven't