How to Build Your First PHP Package
Published on by Paul Redmond
If you want to create a PHP package from scratch and share it with other PHP developers, Composer is a dependency manager that makes the process easy! Thanks to Composer, PHP has one of the top package ecosystems. Let's dive in together and walk through the steps to create a PHP package.
Getting Started
This article's primary focus is helping those new to PHP (or new to writing PHP packages) who want to learn how to create PHP packages from scratch.
There are a few things we need to accomplish as part of setting up a new PHP package:
- Initialize a Git repository
- Create and configure a
composer.json
file - Install dependencies
- Set up autoloading
While we could start by creating an empty GitHub project and cloning it locally, we will instead create a new folder locally, initialize the project, and then push the source code to GitHub afterward:
$ mkdir example-package$ cd ./example-package$ git init$ echo "/vendor/" >> .gitignore$ composer init$ git add .$ git commit -m"First Commit"# later you can add a remote and push the source code
The composer init
command will walk you through setting up your project interactively, setting values such as the package name, authors, and license, and searching for package dependencies. Feel free to fill these out, but for the sake of brevity, here's our starting point:
{ "name": "laravelnews/feeds", "description": "Get articles from Laravel-News.com", "type": "library", "require": {}}
We have the basic configuration for a package, but it won't accomplish much. Some packages don't need to require any dependencies if the package will only use the core PHP language. Regardless, you'll need to set up autoloading so your package users can load the functions and classes in their projects.
When you're ready to hook up your local checkout to a VCS like GitHub, you can follow the instructions for adding a remote. It might look similar to the following command:
git remote add origin git@github.com:laravelnews/example-package.git
Setting Up Autoloading
After creating the basic composer.json
structure, we can move on to creating the source code. You'll need to decide where you want to store the source code within your project. The folder can be called anything you want, but the typical "standard" is src/
or lib/
. Composer doesn't care which path(s) you use, however, you do need to instruct Composer where it can autoload files using PSR-4. Let's use the src
folder and create a class for our example package:
$ mkdir src/$ touch src/Api.php
Next, open the composer.json
file and configure the autoloader using the "autoload"
key:
{ "name": "laravelnews/feeds", "description": "Get articles from Laravel-News.com", "type": "library", "require": {}, "autoload": { "psr-4": { "LaravelNews\\Feed\\": "src/" } }}
The properties within the autoload.psr-4
key map PHP namespaces to folders. When we create files in the src
folder, they will be mapped to the LaravelNews\Feed
namespace. For this example, we created a Api.php
file that requests and returns the Laravel News JSON feed. If you're following along, add the following code to src/Api.php
:
<?php namespace LaravelNews\Feed; class Api{ public function json(): array { $json = file_get_contents('https://laravel-news.com/feed/json'); return json_decode($json, true); }}
How can we try our new class out right now?
There are a few ways, such as requiring this package in another project via local Composer dependencies or even pushing the code up to GitHub and doing a composer update
on our package using dev-main
. However, we can also just create an index.php
file in the root of the project to try it out:
use LaravelNews\Feed\Api; require __DIR__.'/vendor/autoload.php'; $response = (new Api)->json(); echo "The Laravel-News.com feed has returned ".count($response['items']['items'])." items.\n";// ...
We required Composer's autoloader, which knows how to load the files for our package. For Composer to understand how to find our files, we need to run composer install
:
$ composer install# or$ composer dump-autoload$ php index.phpThe Laravel-News.com feed has returned 20 items.
You can also run the dump-autoload
command to update Composer's autoloader after adding the namespace to composer.json
.
Running the index.php
file lets us quickly start working with our package, however, we can also start using our code by creating a test suite. Let's dive in to setting it up!
Package Tests and Development Dependencies
I recommend writing tests for any project you work on, and I like setting up tests as early as possible. When creating a PHP package, the most common test framework is PHPUnit. My favorite option lately is Pest PHP, and I think you'll love how easy it is to set up!
Composer packages have two sets of requirements: the require
section includes packages that are required for your package to run, and require-dev
includes packages that are required for testing. Thus far, we don't have any require
packages, and that might happen if you don't want or need any other package dependencies.
I doubt that you want to write your own testing framework from scratch, so we're about to install our first development dependency. We also don't always want to make requests to a live JSON endpoint, so we will also install a mocking library (Mockery) to mock HTTP calls:
$ composer require pestphp/pest --dev --with-all-dependencies$ composer require --dev mockery/mockery
Tip: I recommend configuring package sorting to keep your dependencies organized via the following configuration option in composer.json
:
"config": { "sort-packages": true}
After installing Pest and Mockery, we can initialize Pest via the --init
flag. Once the files are created, we can run pest
to test our code:
vendor/bin/pest --init# ...vendor/bin/pest PASS Tests\Feature\ExampleTest ✓ example PASS Tests\Unit\ExampleTest ✓ example Tests: 2 passed (2 assertions) Duration: 0.06s
You can organize your package's tests in whatever manner you want, and I recommend checking out the Pest Documentation for full details on setting up Pest.
Next, let's create a simple class we can use to demonstrate a package test. This class will get recent articles from the Laravel News JSON feed and return the latest article.
We will call this fictitious class NewsChecker
and add it to the src/NewsChecker.php
file with the following contents:
<?php namespace LaravelNews\Feed; class NewsChecker{ public function __construct( private Api $api ) {} public function latestArticle(): array { $response = $this->api->json(); $items = $response['items']['items'] ?? []; if (empty($items)) { throw new \Exception("Unable to retrieve the latest article from Laravel-News.com"); } usort($items, function($a, $b) { return strtotime($b['date_published']) - strtotime($a['date_published']); }); return $items[0]; }}
Note that it takes the Api
class as a dependency, which we will mock in our test.
Next, we'll create this file at tests/Feature/NewsCheckerTest.php
file and add the following tests to validate the latestArticle()
method:
use LaravelNews\Feed\Api;use LaravelNews\Feed\NewsChecker; it('Returns the latest article on Laravel-News.com', function () { $items = [ [ 'id' => 3648, 'title' => "Laravel SEO made easy with the Honeystone package", 'date_published' => "2024-08-20T13:00:00+00:00", ], [ 'id' => 3650, 'title' => "LCS #5 - Patricio: Mingle JS, PHP WASM, VoxPop", 'date_published' => "2024-08-23T13:00:00+00:00", ], [ 'id' => 3647, 'title' => "Laravel Model Tips", 'date_published' => "2024-08-22T13:00:00+00:00", ], ]; $api = Mockery::mock(Api::class); $api->shouldReceive('json')->once()->andReturn([ 'title' => 'Laravel News Feed', 'feed_url' => 'https://laravel-news.com/feed/json', 'items' => [ 'items' => $items, ], ]); $checker = new NewsChecker($api); $article = $checker->latestArticle(); expect($article['title'])->toBe("LCS #5 - Patricio: Mingle JS, PHP WASM, VoxPop");}); it('Throws an exception if no items are returned from the feed', function () { $api = Mockery::mock(Api::class); $api->shouldReceive('json')->once()->andReturn([ 'title' => 'Laravel News Feed', 'feed_url' => 'https://laravel-news.com/feed/json', ]); $checker = new NewsChecker($api); expect(fn() => $checker->latestArticle()) ->toThrow(new Exception('Unable to retrieve the latest article from Laravel-News.com'));});
You can run these tests and validate that the code works by running vendor/bin/pest
. Feel free to delete the example tests created after running pest --init
.
We've covered quite a bit of ground, from initializing a Git repository, configuring the PHP package with composer.json
, adding source code and tests, and running them with Pest. From here, you're ready to publish your package on Packagist!
Learn More
I recommend signing up and checking out the documentation on Packagist.org, where you'll publish new versions of your package. The process of updating your package versions on Packagist can be automated, meaning that when you tag new versions of your package, they will automatically show up on Packagist.org.
We walked through creating a package from scratch, but if you are using GitHub, creating a template repository for your organization or personal projects can speed things up even more! There are some community standout package skeletons that you can use as a starting point for your next Composer package: