Building a Simple PHP Composer Package
Make it clean, make it shareable: writing your own Composer package with good organized structure.
Introduction
Why we need composer packages?
Composer is the de facto package manager for PHP, making it easy to share and reuse code across projects. By creating a reusable Composer package, you can modularize your code and avoid “reinventing the wheel” in future projects. It’s also satisfying and fun to open-source your work – not only do you benefit from reusability, but other developers can discover and use your package, potentially improving it with feedback or contributions. In the vast PHP ecosystem, thousands of packages on Packagist (the main Composer repository) exemplify how sharing solutions benefits the community. In short, building a package encourages better code organization, and publishing it publicly gives others the opportunity to learn from or build upon your work.
What inside this article?
In this article, we’ll walk through how to build a simple PHP package and publish it on Packagist, using a real example: a “Postal Formatter” library that we’ve built to use in different projects. This package provides a handy utility to format European postal codes according to each country’s official standards. We’ll cover the journey of creating the package, setting it up for Composer, pushing it to GitHub, releasing it via Packagist, and even highlight a neat PHP 8.1 feature (Enums) used in the package. By the end, you should have a clear roadmap for creating and sharing your own PHP library.
Bring it to the world!
So let’s start with the publishing process.
After writing your PHP library, how do you make it installable via Composer for anyone in the world? The answer is Packagist.org – the central repository for Composer packages.
The process is quite straightforward described below and can’t be represented as to-do guide with some steps:
Prepare a GitHub Repository: Create a new public repository on GitHub for your package code. At minimum, your repo should contain a composer.json file at its root with the package’s metadata (name, description, author, PHP version requirement, etc.). This file is essential – Packagist will read it to get your package info . For example, your composer.json should declare a unique package name in the format "vendor/package" (e.g., "labrodev/postal-formatter") and the minimum PHP version, among other details. You should also include a README with usage instructions and a License file (more on these later).
Implement Your Package Code: Develop your library code under the repository. Follow PSR-4 autoloading by organizing classes into directories matching your namespace (configured in composer.json). For instance, if your package’s namespace is Labrodev\PostalFormatter, the source files might live under a src/ directory. Ensure your code is committed and pushed to the GitHub repo.
Add a Version Tag: Before submitting to Packagist, tag a release version in your Git repository. This step assigns a version number to your code. Using semantic versioning is recommended (e.g., v1.0.0 for your first release). You can create a tag via command line:
git tag -a v1.0.0 -m "Initial release" git push origin v1.0.0
Packagist automatically detects versions from Git tags. In fact, new versions of your package are fetched from the tags you create in your VCS repository, so it’s best to omit a fixed version in composer.json and just rely on Git tags like 1.0.0 or v1.0.0 . This way, each Git tag (v1.0.1, v1.1.0, etc.) will appear as a version that users can require via Composer.
Submit to Packagist: Create an account on Packagist.org (you can sign in with GitHub for convenience). Once logged in, click the “Submit” button on Packagist’s top menu. In the submission form, provide the public Git repository URL of your package (for example, the GitHub URL). Packagist will import your repository – reading the composer.json to gather package details automatically . After submitting, your package will have its own Packagist page, and developers can install it via Composer.
Enable Auto-Updates (Optional but Recommended): By default, Packagist will periodically check your repo for new tags. To get immediate updates on new releases, you can hook up Packagist to your GitHub. If you logged in via GitHub and authorized Packagist, it can set up a service hook so that every time you push a new tag, Packagist notices right away . This ensures your users can fetch the latest version as soon as you release it.
Once these steps are done, your library is live on Packagist! For example, if your package is named vendor/package, anyone can run composer require vendor/package to pull it into their project . In our case, we used labrodev/postal-formatter as the package name, so developers can install it with a simple command:
composer require labrodev/postal-formatter
Next, let’s dive into the Postal-Formatter package itself as a case study of how a simple Composer package is structured and what it contains.
Case Study: Inside the Postal-Formatter Package
Postal-Formatter is a small PHP library that formats European postal codes in a consistent way. Imagine you have postal/zip codes from various countries – this tool will clean them up and output them in the official format for that country. For example, if given a UK code "sw1a1aa", the library will output "SW1A 1AA" with the proper capitalization and spacing; if given a Czech Republic code "12345", it will output "123 45" with the standard space in the middle . It supports over 45 country formats (essentially most of Europe) and normalizes input by removing any extraneous characters and uppercasing letters . In short, it’s a handy utility if you’re dealing with international postal addresses and want to ensure the codes are uniformly formatted.
Features and Usage
To summarize what our tiny package offers, here are its key features (as described in the README):
Cleans and standardizes postal code input (trims whitespace, removes non-alphanumeric characters, and uppercases letters) .
Supports 45+ European country codes, each with country-specific formatting rules (ISO 3166-1 alpha-2 country codes are used for identifying countries) .
Auto-formats codes per country standard – e.g., "12345" becomes "123 45" for Czech Republic (CZ), "LV1234" becomes "LV-1234" for Latvia, or "sw1a1aa" becomes "SW1A 1AA" for Great Britain (GB) .
Provides a simple static interface for formatting (a single PostalFormatter::format() method handles everything).
Written for strict typing in PHP 8.1+ (uses declare(strict_types=1) and typed class properties/methods), ensuring type safety.
Includes a test suite (PHPUnit) and static analysis (PHPStan) configuration, which means the code is verified for correctness and adherence to best practices.
Using the package is straightforward. We just call the static format method. For example:
use Labrodev\PostalFormatter\Utilities\PostalFormatter;
echo PostalFormatter::format(' 12345 ');
// Outputs: 12345 (default normalization)
echo PostalFormatter::format('12345', 'CZ');
// Outputs: 123 45 (Czech format)
echo PostalFormatter::format('sw1a1aa', 'GB');
// Outputs: SW1A 1AA (UK format)
echo PostalFormatter::format('1050', 'LV');
// Outputs: LV-1050 (Latvia format)
Under the hood, the formatter will throw an exception if you provide an unsupported country code (to avoid silently failing or mis-formatting). For instance, PostalFormatter::format('12345', 'XX') would throw an InvalidCountryCode exception, because “XX” is not a valid ISO country code in its list.
Project Structure and Notable Files
One goal of this case study is to illustrate how a simple Composer package is organized. The Postal-Formatter repository follows common best practices for PHP libraries:
composer.json: This file defines the package metadata. It includes the package name (labrodev/postal-formatter), a description, keywords, the minimum PHP version (8.1), autoload information (PSR-4 mapping of the namespace to the src/ directory), and any dependencies. It also lists development requirements like PHPUnit and PHPStan, and even defines convenient Composer scripts (e.g., "test" to run the test suite, "analyse" to run PHPStan) to streamline development. This file is crucial, as Packagist reads it to register your package and Composer uses it to resolve dependencies.
README.md: A comprehensive README is provided, which serves as the documentation for the package. It typically explains the package’s purpose, features, installation instructions, usage examples, and any other important notes. In Postal-Formatter’s README, for example, you’ll find the list of features and code examples we discussed above, plus sections on running tests and static analysis. A good README is important for any open-source package – it’s the first thing developers will read on GitHub or Packagist to understand how to use your library.
CHANGELOG.md (or CHANGES.md): It’s a good practice to include a changelog file listing the changes in each version of your package. This helps users see what’s new or fixed when you tag a new release. In our package, a CHANGES.md file outlines updates across versions (e.g., new country formats added, bug fixes, etc.). Maintaining this is especially useful as your package evolves.
LICENSE: An open-source license file (in this case, MIT License) is included, allowing others to know the terms under which they can use and distribute the package. MIT is a permissive license, encouraging wide usage. Make sure to choose a license for your package – lack of a clear license can deter potential users or contributors.
src/ Directory: This contains the PHP source code for the library, organized by namespace. For Postal-Formatter:
Utilities/PostalFormatter.php – the main utility class with the format() method. It handles cleaning the input and delegating to country-specific formatting if a country code is provided.
Enums/CountryCode.php – an Enum defining supported country codes (like CZ, GB, FR, etc.) with each enum case encapsulating the logic to format a postal code for that country. We’ll discuss this Enum in detail in the next section.
Exceptions/InvalidCountryCode.php – a custom exception class thrown when an unknown country code is used. This is a simple class extending PHP’s Exception, providing a static make($code) method to create a standardized error message.
tests/ Directory: Contains unit tests (using PHPUnit) to verify the package’s behavior. For instance, Postal-Formatter has tests ensuring that PostalFormatter::format() returns expected outputs for a variety of inputs and country codes. There’s also a test to ensure an invalid country code triggers the appropriate exception. Automated tests give confidence that the package works as intended and that future changes won’t break existing functionality.
By structuring the project in this way, we ensure that it’s easy to navigate and maintain. When others browse your repository or install the package, they can quickly find the documentation (README), see how to run tests, and understand how the code is organized. These are hallmarks of a well-crafted Composer package.
Quality Assurance: Static Analysis and Testing
As mentioned, our example package emphasizes code quality by including static analysis and testing tools:
PHPUnit Tests: Having a suite of tests is crucial for any package. In Postal-Formatter, the tests cover various country formats (e.g., formatting a Polish postal code should insert a dash after the first two digits, formatting a UK code should insert a space in the right spot, etc.) and edge cases (like handling already formatted input or throwing exceptions on bad input). To run the tests, one can simply execute composer test (thanks to the script in composer.json), which runs PHPUnit. All tests passing means our formatter works for all supported countries as expected.
PHPStan Static Analysis: The package also includes a PHPStan configuration (phpstan.neon.dist) and a Composer script composer analyse to run static analysis. PHPStan goes through the code to catch potential bugs or type errors without even running the code. By running static analysis, we ensure that our code has no obvious mistakes, that we’re not calling undefined methods, passing wrong types, etc. Using PHPStan at level max (or a high level) enforces strict, bug-free code. The inclusion of "declare(strict_types=1)" at the top of PHP files further ensures that type declarations are honored, preventing unintended type juggling. Embracing these tools (tests and static analysis) means the package is more robust and trustworthy for users. It’s great to highlight this in your package’s README (Postal-Formatter’s README explicitly notes it includes PHPUnit and PHPStan support ).
Overall, by investing in testing and static analysis, even a small utility library maintains high quality. When you build your own package, consider doing the same – it pays off in fewer bugs and easier maintenance.
Using PHP 8.1 Enums in Postal-Formatter
One particularly interesting aspect of the Postal-Formatter package is its usage of PHP 8.1 Enums. Enums (enumerations) are a feature introduced in PHP 8.1 that allow you to define a set of constant values in a type-safe way . Unlike traditional constants or configuration arrays, Enums give you a class-like structure where each possible value is a discrete case of the enum type, and you can even attach methods to it.
In Postal-Formatter, the CountryCode enum plays a central role. It defines cases for each supported country (like case CZ = 'CZ';, case GB = 'GB';, etc., with the two-letter codes as values). This enum also includes a method formatPostalCode(string $raw): string which contains the logic to format a cleaned postal code for that specific country. Internally, it uses a match expression to apply the correct pattern. For example:
For countries like Czech Republic (CZ) or Slovakia (SK) that use a 3+2 digit format, the enum’s formatPostalCode returns substr($code,0,3) . ' ' . substr($code,3) (adding a space after the third digit) if the input is 5 digits long.
For the UK (GB), it uses a regular expression to split the code into the outward and inward parts (e.g., "SW1A1AA" -> "SW1A 1AA").
For Latvia (LV), it prepends "LV-" if the code is 4 digits.
And so on for other countries (some use a dash, some have prefixes like AD or MC).
The PostalFormatter::format() function utilizes this enum by attempting to convert the country code string to a CountryCode enum case:
$country = CountryCode::tryFrom($countryCode)
?? throw InvalidCountryCode::make($countryCode);
return $country->formatPostalCode($cleaned);
If tryFrom fails (meaning the provided code isn’t one of the enum cases), it throws our earlier mentioned InvalidCountryCode exception. Otherwise, it calls the enum’s formatting method for that country.
Why Use an Enum for Country Codes?
Enums in PHP 8.1 are a perfect fit when you have a limited, predefined set of values — like country codes. In the case of postal formatting, each country has its own unique rules, so it makes sense to encapsulate those rules alongside the country identifiers themselves.
By using an enum (CountryCode), we get type safety by design: only supported country codes can be used, and any invalid input is caught early. When a user passes a string like "CZ" or "PL", we convert it using CountryCode::tryFrom(), which ensures the value maps to a valid case — otherwise, an exception is thrown. This avoids the pitfalls of dealing with unchecked strings throughout the codebase.
Another major advantage is that each enum case can contain logic. In this package, we’ve attached a formatPostalCode() method directly to the CountryCode enum. That means each country case knows how to format its postal code — no need for a giant switch in a separate service or a hardcoded array of closures. It’s clean, localized, and easy to extend. Want to support a new country? Just add a new case and its formatting logic. Done.
This approach also makes the code much easier to navigate and maintain. You don’t have to look across multiple files or layers to understand how formatting works — it’s all bundled logically with the enum case itself.
In short, using an enum here improves:
Clarity – the code is more readable and expressive
Safety – only valid country codes are accepted
Extensibility – adding new rules is straightforward
Modernity – this is idiomatic PHP 8.1+, using language features as they were meant to be used
For a utility package like Postal-Formatter, this results in a tidy, robust, and maintainable solution.
Conclusion
Building a Composer package in PHP is both rewarding and educational. We started with the motivation – creating reusable code and sharing it with others – and walked through the practical steps of publishing a package on Packagist (from setting up your composer.json to tagging releases and submitting to Packagist). Then we explored the Postal-Formatter package as a concrete example of a simple yet useful library. Along the way, we saw how important it is to structure your package well (with proper documentation, autoloading, and licensing) and to uphold code quality via tests and static analysis.
Finally, we highlighted the use of PHP 8.1 enums in the package, which showcases how embracing new language features can lead to cleaner and more robust solutions. Enums helped ensure only valid country codes are handled and neatly packaged each country’s formatting logic.
If you’re a PHP developer, I encourage you to try creating your own small Composer package. Think of a common problem or utility in your projects, abstract it into a reusable library, and follow the steps to publish it. Not only will it streamline your future development, but you’ll also get the thrill of contributing to the open-source ecosystem. And if you’re curious about the Postal-Formatter package, check it out on GitHub or Packagist – feel free to use it, suggest improvements, or learn from its code. Happy coding, and enjoy your journey into building PHP packages!
Sources:
Official Packagist documentation on submitting packages and versioning
Labrodev Postal-Formatter package on Packagist (README and details)
Picture credits:
Thanks for you attention! Subscribe for Labrodev blog to have more interesting articles around web/php/laravel/vue.js development and more.
You may also share a post if you found it interesting!