How to Build Your First PHP Package

Published on by

How to  Build Your First PHP Package image

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.php
The 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:

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Ben Holmen: Building Connections and Friendships through Pair Programming with Strangers image

Ben Holmen: Building Connections and Friendships through Pair Programming with Strangers

Read article
Laravel Herd Adds Forge Integration, Dump Updates, and More in v1.11 image

Laravel Herd Adds Forge Integration, Dump Updates, and More in v1.11

Read article
Chaperone, Defer, Cache::flexible, and more are now available in Laravel 11.23 image

Chaperone, Defer, Cache::flexible, and more are now available in Laravel 11.23

Read article
UnoPim is a Product Information Management System Built With Laravel image

UnoPim is a Product Information Management System Built With Laravel

Read article
Pest 3 is released! image

Pest 3 is released!

Read article
Build Your Multi-Tenant SaaS App in Days with SaaSykit Tenancy image

Build Your Multi-Tenant SaaS App in Days with SaaSykit Tenancy

Read article