Skip to content
Muhammet Şafak
tr
Tools & Technologies 4 min read

Publishing Your Own PHP Package on Packagist with Composer

I configured a PHP package from scratch and published it on Packagist so others could use it — here's how I did it.


I noticed I had been copying and pasting the same helper class across several projects. Every time, I had to check whether the copy was up to date or outdated. One day I thought, “What if I turned this into a Composer package?” — so I tried it. The result was easier than I expected.

In this post I’ll walk through configuring a PHP package from scratch and publishing it on Packagist (the PHP package registry) step by step. Packagist is the default public repository that Composer looks up.

Package structure

A Composer package is essentially a directory with a specific layout. The basic structure should look like this:

my-package/
├── src/
│   └── HelperClass.php
├── tests/
│   └── HelperClassTest.php
├── composer.json
├── .gitignore
└── README.md

PHP files live under src/, tests under tests/, and composer.json is the central piece that ties everything together.

Preparing composer.json

{
    "name": "muhammetsafak/yardimci",
    "description": "Günlük PHP geliştirmede işime yarayan yardımcı sınıflar",
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "Muhammet Şafak",
            "email": "[email protected]"
        }
    ],
    "require": {
        "php": ">=5.5"
    },
    "require-dev": {
        "phpunit/phpunit": "~4.0"
    },
    "autoload": {
        "psr-4": {
            "Muhammetsafak\\Yardimci\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Muhammetsafak\\Yardimci\\Tests\\": "tests/"
        }
    }
}

A few important points:

  • name: Must follow the vendor/package format and be unique on Packagist.
  • autoload: Uses PSR-4 to map a namespace to a directory. Classes under the Muhammetsafak\Yardimci\ namespace are looked up in src/.
  • require-dev: Dependencies needed only during development go here — they are not downloaded when a consumer runs composer install.

Getting type: "library" right matters. Packagist uses this field to decide how to classify the package. If you’re building a Laravel package, type: "library" is still valid; Laravel-specific service providers are declared in the extra field.

Writing a simple class

<?php

namespace Muhammetsafak\Yardimci;

class Metin
{
    public static function kisalt(string $metin, int $uzunluk = 100, string $son = '...'): string
    {
        if (mb_strlen($metin) <= $uzunluk) {
            return $metin;
        }

        return mb_substr($metin, 0, $uzunluk) . $son;
    }

    public static function slug(string $metin): string
    {
        $metin = mb_strtolower($metin, 'UTF-8');
        $metin = str_replace(
            ['ç', 'ş', 'ğ', 'ı', 'İ', 'ö', 'ü'],
            ['c', 's', 'g', 'i', 'i', 'o', 'u'],
            $metin
        );
        $metin = preg_replace('/[^a-z0-9\s-]/', '', $metin);
        $metin = preg_replace('/[\s-]+/', '-', $metin);

        return trim($metin, '-');
    }
}

One thing I noticed before writing this class: all functions used in a library should be prefixed with mb_. Use mb_strlen instead of strlen, mb_strtolower instead of strtolower. If someone else uses your library and their text data is in an encoding other than UTF-8, plain string functions will silently produce incorrect results.

Pushing to GitHub

Packagist works with GitHub repositories, so the package needs to be pushed to a GitHub repo.

Version tags matter. Composer looks at tags to decide which version to download. Following semantic versioning:

git tag v1.0.0
git push origin v1.0.0

Tags like v1.0.0, v1.0.1, and v1.1.0 are recognized by Composer as versions.

You can also use a package via a branch name like dev-main without tags, but that is not a stable dependency. The consumer’s composer.json would need "minimum-stability": "dev", which is not recommended in practice. Using properly tagged versions ensures that consumers know exactly what code they are getting.

Registering on Packagist

Log in to packagist.org with your GitHub account. On the “Submit” page, paste your GitHub repository URL — Packagist reads composer.json and creates the package.

After that, anyone can add the package to their project with:

composer require muhammetsafak/yardimci

Webhook for automatic updates

You need to set up a webhook so Packagist is notified every time you push an update. Go to the “Webhooks” section in your GitHub repository settings; Packagist provides a URL and a token. Add both as a webhook. From that point on, every git push will trigger an automatic update on Packagist.

Without a webhook, you can manually trigger a “Force Update” from the Packagist dashboard — but that is a one-off solution. A webhook ensures that Packagist reflects the new information within minutes of you tagging a new release.

.gitignore and distribution files

Beyond adding a .gitignore, it is good practice to create a .gitattributes file. In this file you can exclude the test directory and development tooling from distribution archives:

/tests export-ignore
/.github export-ignore
phpunit.xml export-ignore

This way, when a consumer runs composer install --prefer-dist, only the contents of src/ are downloaded — tests and CI configuration are left out. The project’s vendor/ directory doesn’t bloat unnecessarily.

First publish experience

When I pushed the package to Packagist and installed it in another project via composer require, there was a strange feeling: using code I wrote myself as a dependency. For any future update, a single composer update across all projects is all it takes.

I fixed a bug, tagged v1.0.1, pushed it to GitHub. The webhook triggered an immediate update on Packagist. In the dependent projects, composer update muhammetsafak/yardimci pulled the latest version. Going through that cycle for the first time made me realize, once again, just how fragile copy-paste really is.

Writing a library sounds like a big undertaking, but small, single-purpose packages are genuinely useful. Versioned, shareable code instead of copy-paste. Over time, this habit also nudges you toward thinking about code in a more modular way.

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind