Home PHP PHP Package Development with Composer: Build & Publish
Advanced 4 min · July 13, 2026

PHP Package Development with Composer: Build & Publish

Learn to create, publish, and maintain PHP packages with Composer.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Written from production experience, not tutorials.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 20-25 min read
  • 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)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is PHP Package Development with Composer?

PHP package development with Composer is the process of creating reusable, versioned libraries that can be easily installed and managed in PHP projects using the Composer dependency manager.

Think of Composer like an app store for PHP code.
Plain-English First

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.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "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"
        }
    ]
}
💡PSR-4 Autoloading
📊 Production Insight
Always run composer validate to check your composer.json for syntax errors before committing.
🎯 Key Takeaway
A standard package structure with a proper composer.json and PSR-4 autoloading is the foundation of any PHP package.

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!".

src/MyClass.phpPHP
1
2
3
4
5
6
7
8
9
10
11
<?php

namespace YourVendor\YourPackage;

class MyClass
{
    public function greet(string $name): string
    {
        return "Hello, $name!";
    }
}
🔥Namespace Convention
🎯 Key Takeaway
Always match the namespace in your PHP files to the PSR-4 mapping in composer.json.

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.

composer.json (with dependencies)JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
    "name": "your-vendor/your-package",
    "description": "A package that sends HTTP requests",
    "type": "library",
    "autoload": {
        "psr-4": {
            "YourVendor\\YourPackage\\": "src/"
        }
    },
    "require": {
        "php": ">=8.0",
        "guzzlehttp/guzzle": "^7.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5"
    },
    "license": "MIT"
}
⚠ Version Constraints
📊 Production Insight
Always test your package with the lowest and highest supported versions of dependencies to ensure compatibility.
🎯 Key Takeaway
Manage dependencies carefully with appropriate version constraints and separate dev dependencies.

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')); } } ```

``json "autoload-dev": { "psr-4": { "YourVendor\\YourPackage\\Tests\\": "tests/" } } ``

``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.

tests/MyClassTest.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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'));
    }

    public function testGreetWithEmptyString()
    {
        $obj = new MyClass();
        $this->assertEquals('Hello, !', $obj->greet(''));
    }
}
💡Test Coverage
📊 Production Insight
Include a CI configuration file (e.g., .github/workflows/php.yml) to run tests on every commit.
🎯 Key Takeaway
Automated tests ensure your package works as expected and prevent regressions.

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.

  1. Push your package to a public GitHub repository.
  2. Go to [packagist.org](https://packagist.org) and log in with your GitHub account.
  3. Click "Submit" and enter the URL of your GitHub repository.
  4. Packagist will detect the composer.json and create the package.

``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).

Git tag commandBASH
1
2
git tag v1.0.0
git push origin v1.0.0
🔥Semantic Versioning
📊 Production Insight
Set up a Packagist webhook in your GitHub repository to automatically update the package on new tags.
🎯 Key Takeaway
Publishing on Packagist makes your package globally available via Composer.

Best Practices and Security

  • 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.

```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); } } ```

src/SecureStorage.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?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);
    }

    public function decrypt(string $encryptedData): string
    {
        $decoded = base64_decode($encryptedData, true);
        if ($decoded === false || strlen($decoded) < 16) {
            throw new \RuntimeException('Invalid encrypted data.');
        }
        $iv = substr($decoded, 0, 16);
        $encrypted = substr($decoded, 16);
        $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $this->encryptionKey, 0, $iv);
        if ($decrypted === false) {
            throw new \RuntimeException('Decryption failed.');
        }
        return $decrypted;
    }
}
⚠ Security First
📊 Production Insight
Run composer audit regularly to check for vulnerabilities in your dependencies.
🎯 Key Takeaway
Follow security best practices: strict types, input validation, minimal dependencies, and proper encryption.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing Autoloader

Symptom
After deploying a new package, the application threw fatal errors: 'Class "MyVendor\MyPackage\MyClass" not found'.
Assumption
The developer assumed that Composer's autoloader would automatically pick up the new package's classes because it was listed in the main project's composer.json.
Root cause
The package's composer.json had an incorrect autoload configuration. The PSR-4 mapping pointed to a wrong directory, so Composer's autoloader couldn't find the class files.
Fix
Updated the package's composer.json to correctly map the namespace to the 'src/' directory and ran 'composer dump-autoload' in the main project.
Key lesson
  • 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.
Production debug guideSymptom to Action3 entries
Symptom · 01
Class not found error when using a package class.
Fix
Check if the package is installed: 'composer show -i'. Verify autoload configuration in package's composer.json. Run 'composer dump-autoload'.
Symptom · 02
Composer install fails with 'Could not find package'.
Fix
Check package name and version constraints. Ensure the package is published on Packagist or the repository is correctly specified.
Symptom · 03
Package methods behave differently in production.
Fix
Check PHP version differences. Ensure all dependencies are installed. Review environment-specific configurations.
★ Quick Debug Cheat SheetCommon package development issues and immediate fixes.
Class not found
Immediate action
Run 'composer dump-autoload -o'
Commands
composer dump-autoload -o
composer validate
Fix now
Check PSR-4 mapping in composer.json
Package not found on Packagist+
Immediate action
Verify package name and version
Commands
composer search your-vendor/your-package
composer show your-vendor/your-package
Fix now
Publish or update package on Packagist
Dependency conflict+
Immediate action
Check composer.json require section
Commands
composer why package/name
composer update --with-dependencies
Fix now
Adjust version constraints
FeaturePSR-4 AutoloadingClassmap AutoloadingFiles Autoloading
PerformanceFast (lazy loading)Fast (pre-generated map)Fast (always loaded)
FlexibilityHigh (namespace mapping)Low (explicit file list)Low (global functions)
Ease of UseEasy (convention-based)Moderate (requires dump)Easy (list files)
RecommendedYesFor legacy codeFor functions only
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
composer.json{Setting Up Your Package Structure
srcMyClass.phpnamespace YourVendor\YourPackage;Writing Your First Package Class
composer.json (with dependencies){Managing Dependencies
testsMyClassTest.phpnamespace YourVendor\YourPackage\Tests;Testing Your Package
Git tag commandgit tag v1.0.0Publishing Your Package on Packagist
srcSecureStorage.phpdeclare(strict_types=1);Best Practices and Security

Key takeaways

1
A PHP package is a reusable library with a composer.json that defines metadata, autoloading, and dependencies.
2
Use PSR-4 autoloading to map namespaces to directories for efficient class loading.
3
Follow semantic versioning and include a README, license, and tests for a professional package.
4
Publish on Packagist to make your package globally available via Composer.
5
Prioritize security
validate inputs, use strict types, and keep dependencies minimal.

Common mistakes to avoid

3 patterns
×

Forgetting 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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain PSR-4 autoloading and how it works with Composer.
Q02JUNIOR
What is semantic versioning and why is it important for PHP packages?
Q03SENIOR
How would you handle a situation where your package depends on a library...
Q01 of 03SENIOR

Explain PSR-4 autoloading and how it works with Composer.

ANSWER
PSR-4 is an autoloading standard that maps fully qualified class names to file paths. In composer.json, you define a namespace prefix and a base directory. When a class is requested, Composer replaces the namespace prefix with the base directory and converts the rest of the class name to a file path. For example, YourVendor\YourPackage\MyClass maps to src/MyClass.php if the prefix is YourVendor\YourPackage\ and the base directory is src/.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between `require` and `require-dev` in composer.json?
02
How do I update my package on Packagist after making changes?
03
Can I create a private package that is not on Packagist?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

4 min read · try the examples if you haven't

Previous
PHP Code Quality: PHPStan, Rector, Pint, and PHP CS Fixer
29 / 29 · Advanced PHP