1. php
  2. /packages
  3. /publishing

Publishing PHP Packages

Introduction to Package Publishing

Publishing your PHP package makes it available to the global PHP community through Composer and Packagist. This guide covers the entire publishing process, from preparation to maintenance and promotion.

Pre-Publishing Checklist

Code Quality Standards

# Run quality checks before publishing
composer cs-check     # Code style
composer analyze      # Static analysis
composer test         # Test suite
composer test-coverage # Coverage check

Required Files

my-package/
├── composer.json      # Package metadata (required)
├── README.md         # Usage documentation (required)
├── LICENSE          # License file (required)
├── CHANGELOG.md     # Version history (recommended)
├── CONTRIBUTING.md  # Contribution guidelines (recommended)
├── .gitignore       # Git ignore rules (recommended)
└── src/             # Source code (required)

Composer.json Validation

{
    "name": "vendor/package-name",
    "description": "Clear, concise description of your package",
    "type": "library",
    "license": "MIT",
    "keywords": ["php", "library", "utility", "calculator"],
    "homepage": "https://github.com/vendor/package-name",
    "support": {
        "issues": "https://github.com/vendor/package-name/issues",
        "wiki": "https://github.com/vendor/package-name/wiki",
        "docs": "https://docs.example.com",
        "source": "https://github.com/vendor/package-name"
    },
    "authors": [
        {
            "name": "Your Name",
            "email": "[email protected]",
            "homepage": "https://yourwebsite.com",
            "role": "Developer"
        }
    ],
    "require": {
        "php": "^8.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.0"
    },
    "autoload": {
        "psr-4": {
            "Vendor\\PackageName\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Vendor\\PackageName\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "cs-check": "phpcs",
        "cs-fix": "phpcbf"
    },
    "config": {
        "sort-packages": true
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}

Validate Composer Configuration

# Validate composer.json syntax
composer validate

# Validate with strict checking
composer validate --strict

# Check for security issues
composer audit

Publishing to Packagist

Create Packagist Account

  1. Visit packagist.org
  2. Register with GitHub/GitLab account
  3. Verify email address
  4. Set up profile information

Submit Package

# Ensure code is pushed to GitHub
git add .
git commit -m "Prepare for initial release"
git tag v1.0.0
git push origin main --tags

# Submit to Packagist
# 1. Go to packagist.org/packages/submit
# 2. Enter repository URL: https://github.com/vendor/package-name
# 3. Click "Check" to validate
# 4. Submit package

Package Validation Script

<?php
// scripts/validate-package.php
class PackageValidator
{
    private array $errors = [];
    private array $warnings = [];
    
    public function validate(): bool
    {
        $this->validateComposerJson();
        $this->validateRequiredFiles();
        $this->validateSourceCode();
        $this->validateTests();
        $this->validateDocumentation();
        
        return empty($this->errors);
    }
    
    private function validateComposerJson(): void
    {
        if (!file_exists('composer.json')) {
            $this->errors[] = 'composer.json file is missing';
            return;
        }
        
        $composer = json_decode(file_get_contents('composer.json'), true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            $this->errors[] = 'composer.json is not valid JSON';
            return;
        }
        
        $required = ['name', 'description', 'license', 'autoload'];
        foreach ($required as $field) {
            if (!isset($composer[$field])) {
                $this->errors[] = "Missing required field in composer.json: {$field}";
            }
        }
        
        // Validate package name format
        if (isset($composer['name']) && !preg_match('/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*$/', $composer['name'])) {
            $this->errors[] = 'Package name format is invalid';
        }
        
        // Check for recommended fields
        $recommended = ['keywords', 'homepage', 'support', 'authors'];
        foreach ($recommended as $field) {
            if (!isset($composer[$field])) {
                $this->warnings[] = "Recommended field missing in composer.json: {$field}";
            }
        }
    }
    
    private function validateRequiredFiles(): void
    {
        $required = ['README.md', 'LICENSE'];
        foreach ($required as $file) {
            if (!file_exists($file)) {
                $this->errors[] = "Required file missing: {$file}";
            }
        }
        
        $recommended = ['CHANGELOG.md', 'CONTRIBUTING.md', '.gitignore'];
        foreach ($recommended as $file) {
            if (!file_exists($file)) {
                $this->warnings[] = "Recommended file missing: {$file}";
            }
        }
    }
    
    private function validateSourceCode(): void
    {
        if (!is_dir('src')) {
            $this->errors[] = 'Source directory (src/) is missing';
            return;
        }
        
        $phpFiles = glob('src/**/*.php');
        if (empty($phpFiles)) {
            $this->errors[] = 'No PHP files found in src/ directory';
        }
        
        foreach ($phpFiles as $file) {
            $this->validatePhpFile($file);
        }
    }
    
    private function validatePhpFile(string $file): void
    {
        $content = file_get_contents($file);
        
        // Check for PHP opening tag
        if (!str_starts_with($content, '<?php')) {
            $this->errors[] = "File {$file} missing PHP opening tag";
        }
        
        // Check for namespace
        if (!preg_match('/namespace\s+[a-zA-Z_\\\\]+;/', $content)) {
            $this->warnings[] = "File {$file} missing namespace declaration";
        }
        
        // Basic syntax check
        $check = shell_exec("php -l {$file} 2>&1");
        if (strpos($check, 'No syntax errors') === false) {
            $this->errors[] = "Syntax error in {$file}: {$check}";
        }
    }
    
    private function validateTests(): void
    {
        if (!is_dir('tests')) {
            $this->warnings[] = 'Tests directory missing';
            return;
        }
        
        $testFiles = glob('tests/**/*Test.php');
        if (empty($testFiles)) {
            $this->warnings[] = 'No test files found';
        }
        
        if (!file_exists('phpunit.xml') && !file_exists('phpunit.xml.dist')) {
            $this->warnings[] = 'PHPUnit configuration file missing';
        }
    }
    
    private function validateDocumentation(): void
    {
        if (!file_exists('README.md')) {
            return; // Already flagged as error
        }
        
        $readme = file_get_contents('README.md');
        
        $sections = ['Installation', 'Usage', 'License'];
        foreach ($sections as $section) {
            if (stripos($readme, $section) === false) {
                $this->warnings[] = "README.md missing {$section} section";
            }
        }
        
        // Check for installation instructions
        if (stripos($readme, 'composer require') === false) {
            $this->warnings[] = 'README.md missing Composer installation instructions';
        }
    }
    
    public function getErrors(): array
    {
        return $this->errors;
    }
    
    public function getWarnings(): array
    {
        return $this->warnings;
    }
    
    public function report(): void
    {
        if (!empty($this->errors)) {
            echo "❌ Validation Errors:\n";
            foreach ($this->errors as $error) {
                echo "  - {$error}\n";
            }
            echo "\n";
        }
        
        if (!empty($this->warnings)) {
            echo "⚠️  Warnings:\n";
            foreach ($this->warnings as $warning) {
                echo "  - {$warning}\n";
            }
            echo "\n";
        }
        
        if (empty($this->errors) && empty($this->warnings)) {
            echo "✅ Package validation passed!\n";
        } elseif (empty($this->errors)) {
            echo "✅ Package is ready for publishing (with warnings)\n";
        } else {
            echo "❌ Package is not ready for publishing\n";
        }
    }
}

$validator = new PackageValidator();
$isValid = $validator->validate();
$validator->report();

exit($isValid ? 0 : 1);
?>

Automated Publishing Workflow

GitHub Actions for Publishing

# .github/workflows/publish.yml
name: Publish Package

on:
  push:
    tags:
      - 'v*'

jobs:
  validate:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: 8.1
        
    - name: Install dependencies
      run: composer install --no-dev --optimize-autoloader
      
    - name: Validate package
      run: |
        php scripts/validate-package.php
        composer validate --strict
        
    - name: Run tests
      run: composer test
      
    - name: Check code style
      run: composer cs-check

  publish:
    needs: validate
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      
    - name: Create GitHub Release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        body: |
          Release notes for ${{ github.ref }}
          
          See [CHANGELOG.md](CHANGELOG.md) for details.
        draft: false
        prerelease: false
        
    - name: Trigger Packagist update
      run: |
        curl -XPOST -H'content-type:application/json' \
          "https://packagist.org/api/update-package?username=${{ secrets.PACKAGIST_USERNAME }}&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
          -d'{"repository":{"url":"${{ github.server_url }}/${{ github.repository }}"}}'

Packagist Auto-Update Setup

# Configure Packagist webhook for automatic updates
# 1. Go to your package page on Packagist
# 2. Click "Settings" tab
# 3. Enable "Auto-update packages"
# 4. Add GitHub webhook URL to your repository settings

Distribution Strategies

Multiple Package Repositories

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://packages.example.com"
        },
        {
            "type": "vcs",
            "url": "https://github.com/vendor/package-name"
        }
    ]
}

Private Package Distribution

<?php
// config/satis.json - For private package repository
{
    "name": "My Company Packages",
    "homepage": "https://packages.mycompany.com",
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/mycompany/private-package-1"
        },
        {
            "type": "vcs", 
            "url": "https://github.com/mycompany/private-package-2"
        }
    ],
    "require-all": true,
    "archive": {
        "directory": "dist",
        "format": "tar",
        "prefix-url": "https://packages.mycompany.com",
        "skip-dev": true
    }
}
?>

Docker-based Package Building

# Dockerfile for package building
FROM composer:2 AS builder

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts

COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative

FROM php:8.1-cli-alpine
WORKDIR /app
COPY --from=builder /app .

ENTRYPOINT ["php"]

Package Maintenance

Update Management

<?php
// scripts/update-checker.php
class UpdateChecker
{
    private string $packageName;
    private string $currentVersion;
    
    public function __construct(string $packageName)
    {
        $this->packageName = $packageName;
        $this->currentVersion = $this->getCurrentVersion();
    }
    
    public function checkForUpdates(): array
    {
        $packagistData = $this->getPackagistData();
        $availableVersions = array_keys($packagistData['package']['versions']);
        
        // Filter out dev versions
        $stableVersions = array_filter($availableVersions, function($version) {
            return !str_contains($version, 'dev') && !str_contains($version, 'alpha') && !str_contains($version, 'beta');
        });
        
        usort($stableVersions, 'version_compare');
        $latestVersion = end($stableVersions);
        
        return [
            'current' => $this->currentVersion,
            'latest' => $latestVersion,
            'has_update' => version_compare($this->currentVersion, $latestVersion, '<'),
            'all_versions' => $stableVersions
        ];
    }
    
    private function getCurrentVersion(): string
    {
        $composer = json_decode(file_get_contents('composer.json'), true);
        return $composer['version'] ?? '0.0.0';
    }
    
    private function getPackagistData(): array
    {
        $url = "https://packagist.org/packages/{$this->packageName}.json";
        $response = file_get_contents($url);
        
        if ($response === false) {
            throw new RuntimeException("Failed to fetch package data from Packagist");
        }
        
        return json_decode($response, true);
    }
    
    public function generateUpdateNotification(): string
    {
        $updateInfo = $this->checkForUpdates();
        
        if (!$updateInfo['has_update']) {
            return "Package {$this->packageName} is up to date (v{$updateInfo['current']})";
        }
        
        return "Update available for {$this->packageName}: v{$updateInfo['current']} → v{$updateInfo['latest']}";
    }
}

// Usage
$checker = new UpdateChecker('vendor/package-name');
echo $checker->generateUpdateNotification();
?>

Dependency Security Monitoring

<?php
// scripts/security-check.php
class SecurityChecker
{
    public function checkVulnerabilities(): array
    {
        $composerLock = json_decode(file_get_contents('composer.lock'), true);
        $packages = $composerLock['packages'] ?? [];
        
        $vulnerabilities = [];
        
        foreach ($packages as $package) {
            $vulns = $this->checkPackageVulnerabilities($package['name'], $package['version']);
            if (!empty($vulns)) {
                $vulnerabilities[$package['name']] = $vulns;
            }
        }
        
        return $vulnerabilities;
    }
    
    private function checkPackageVulnerabilities(string $package, string $version): array
    {
        // Using Packagist security API (example)
        $url = "https://packagist.org/api/security-advisories?packages[]={$package}";
        
        $context = stream_context_create([
            'http' => [
                'timeout' => 10,
                'user_agent' => 'Package Security Checker/1.0'
            ]
        ]);
        
        $response = @file_get_contents($url, false, $context);
        
        if ($response === false) {
            return [];
        }
        
        $data = json_decode($response, true);
        $advisories = $data['advisories'] ?? [];
        
        $applicableAdvisories = [];
        foreach ($advisories as $advisory) {
            if ($this->isVersionAffected($version, $advisory['affected_versions'])) {
                $applicableAdvisories[] = [
                    'title' => $advisory['title'],
                    'severity' => $advisory['severity'],
                    'cve' => $advisory['cve'],
                    'link' => $advisory['link']
                ];
            }
        }
        
        return $applicableAdvisories;
    }
    
    private function isVersionAffected(string $version, string $affectedVersions): bool
    {
        // Simplified version constraint checking
        // In practice, you'd use a proper constraint parser
        return str_contains($affectedVersions, $version);
    }
    
    public function generateSecurityReport(): string
    {
        $vulnerabilities = $this->checkVulnerabilities();
        
        if (empty($vulnerabilities)) {
            return "✅ No known security vulnerabilities found.";
        }
        
        $report = "🔒 Security Vulnerabilities Found:\n\n";
        
        foreach ($vulnerabilities as $package => $vulns) {
            $report .= "Package: {$package}\n";
            foreach ($vulns as $vuln) {
                $report .= "  - {$vuln['title']} (Severity: {$vuln['severity']})\n";
                $report .= "    CVE: {$vuln['cve']}\n";
                $report .= "    Link: {$vuln['link']}\n\n";
            }
        }
        
        return $report;
    }
}

$checker = new SecurityChecker();
echo $checker->generateSecurityReport();
?>

Package Promotion

Marketing Strategies

Community Engagement

# Package Promotion Checklist

## Pre-Launch
- [ ] Create compelling README with clear value proposition
- [ ] Set up comprehensive documentation
- [ ] Ensure high test coverage (>80%)
- [ ] Create usage examples and tutorials
- [ ] Set up continuous integration

## Launch
- [ ] Announce on relevant PHP forums and communities
- [ ] Submit to PHP weekly newsletters
- [ ] Share on social media (Twitter, LinkedIn)
- [ ] Write blog post about the package
- [ ] Create demonstration videos

## Post-Launch
- [ ] Respond to issues and pull requests promptly
- [ ] Engage with users in discussions
- [ ] Collect and implement user feedback
- [ ] Regular updates and feature additions
- [ ] Monitor package statistics and usage

Content Marketing

<?php
// Generate package statistics for marketing
class PackageStats
{
    private string $packageName;
    
    public function __construct(string $packageName)
    {
        $this->packageName = $packageName;
    }
    
    public function getPackagistStats(): array
    {
        $url = "https://packagist.org/packages/{$this->packageName}.json";
        $response = file_get_contents($url);
        $data = json_decode($response, true);
        
        $package = $data['package'];
        
        return [
            'downloads' => [
                'total' => $package['downloads']['total'],
                'monthly' => $package['downloads']['monthly'],
                'daily' => $package['downloads']['daily']
            ],
            'stars' => $package['github_stars'] ?? 0,
            'forks' => $package['github_forks'] ?? 0,
            'watchers' => $package['github_watchers'] ?? 0,
            'open_issues' => $package['github_open_issues'] ?? 0,
            'dependents' => $package['dependents'] ?? 0
        ];
    }
    
    public function generateStatsMarkdown(): string
    {
        $stats = $this->getPackagistStats();
        
        return "
## Package Statistics

- **Total Downloads**: " . number_format($stats['downloads']['total']) . "
- **Monthly Downloads**: " . number_format($stats['downloads']['monthly']) . "
- **GitHub Stars**: " . number_format($stats['stars']) . "
- **Dependents**: " . number_format($stats['dependents']) . "

*Last updated: " . date('Y-m-d') . "*
";
    }
    
    public function generateBadgeUrls(): array
    {
        return [
            'downloads' => "https://img.shields.io/packagist/dt/{$this->packageName}.svg",
            'version' => "https://img.shields.io/packagist/v/{$this->packageName}.svg",
            'license' => "https://img.shields.io/packagist/l/{$this->packageName}.svg",
            'php_version' => "https://img.shields.io/packagist/php-v/{$this->packageName}.svg"
        ];
    }
}

$stats = new PackageStats('vendor/package-name');
echo $stats->generateStatsMarkdown();
?>

Community Building

Issue Templates

<!-- .github/ISSUE_TEMPLATE/bug_report.md -->
---
name: Bug report
about: Create a report to help us improve
title: '[BUG] '
labels: bug
assignees: ''
---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Install package with '...'
2. Run code '...'
3. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Code Example**
```php
// Your code example here

Environment:

  • PHP Version: [e.g. 8.1]
  • Package Version: [e.g. 1.2.3]
  • OS: [e.g. Ubuntu 20.04]

Additional context Add any other context about the problem here.


#### Feature Request Template

```markdown
<!-- .github/ISSUE_TEMPLATE/feature_request.md -->
---
name: Feature request
about: Suggest an idea for this project
title: '[FEATURE] '
labels: enhancement
assignees: ''
---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions.

**Additional context**
Add any other context or screenshots about the feature request here.

**Would you be willing to implement this feature?**
- [ ] Yes, I can submit a pull request
- [ ] Yes, but I need guidance
- [ ] No, I cannot implement this

Analytics and Monitoring

<?php
// scripts/package-analytics.php
class PackageAnalytics
{
    private string $packageName;
    private array $data = [];
    
    public function __construct(string $packageName)
    {
        $this->packageName = $packageName;
    }
    
    public function collectData(): void
    {
        $this->data['packagist'] = $this->getPackagistData();
        $this->data['github'] = $this->getGitHubData();
        $this->data['downloads'] = $this->getDownloadTrends();
    }
    
    private function getPackagistData(): array
    {
        $url = "https://packagist.org/packages/{$this->packageName}.json";
        $response = file_get_contents($url);
        return json_decode($response, true)['package'];
    }
    
    private function getGitHubData(): array
    {
        // GitHub API integration
        $repo = str_replace('/', '%2F', $this->packageName);
        $url = "https://api.github.com/repos/{$repo}";
        
        $context = stream_context_create([
            'http' => [
                'header' => 'User-Agent: Package Analytics/1.0'
            ]
        ]);
        
        $response = file_get_contents($url, false, $context);
        return json_decode($response, true);
    }
    
    private function getDownloadTrends(): array
    {
        // Implement download trend analysis
        return [
            'growth_rate' => $this->calculateGrowthRate(),
            'peak_downloads' => $this->findPeakDownloads(),
            'trending' => $this->isTrending()
        ];
    }
    
    private function calculateGrowthRate(): float
    {
        // Calculate month-over-month growth
        $monthly = $this->data['packagist']['downloads']['monthly'] ?? 0;
        $total = $this->data['packagist']['downloads']['total'] ?? 1;
        
        return ($monthly / max($total - $monthly, 1)) * 100;
    }
    
    private function findPeakDownloads(): int
    {
        return $this->data['packagist']['downloads']['daily'] ?? 0;
    }
    
    private function isTrending(): bool
    {
        return $this->calculateGrowthRate() > 10; // 10% growth threshold
    }
    
    public function generateReport(): string
    {
        $this->collectData();
        
        $report = "# Package Analytics Report\n\n";
        $report .= "Package: {$this->packageName}\n";
        $report .= "Generated: " . date('Y-m-d H:i:s') . "\n\n";
        
        $report .= "## Download Statistics\n";
        $report .= "- Total: " . number_format($this->data['packagist']['downloads']['total']) . "\n";
        $report .= "- Monthly: " . number_format($this->data['packagist']['downloads']['monthly']) . "\n";
        $report .= "- Daily: " . number_format($this->data['packagist']['downloads']['daily']) . "\n";
        $report .= "- Growth Rate: " . round($this->data['downloads']['growth_rate'], 2) . "%\n\n";
        
        $report .= "## GitHub Statistics\n";
        $report .= "- Stars: " . number_format($this->data['github']['stargazers_count']) . "\n";
        $report .= "- Forks: " . number_format($this->data['github']['forks_count']) . "\n";
        $report .= "- Open Issues: " . number_format($this->data['github']['open_issues_count']) . "\n\n";
        
        $report .= "## Trends\n";
        $report .= "- Trending: " . ($this->data['downloads']['trending'] ? 'Yes' : 'No') . "\n";
        
        return $report;
    }
}

$analytics = new PackageAnalytics('vendor/package-name');
echo $analytics->generateReport();
?>

Best Practices for Publishers

Package Naming

  • Use descriptive, memorable names
  • Follow kebab-case convention
  • Avoid generic names that conflict with existing packages
  • Consider the vendor namespace carefully

Version Management

  • Follow semantic versioning strictly
  • Use pre-release versions for testing
  • Document breaking changes clearly
  • Provide migration guides for major updates

Community Engagement

  • Respond to issues promptly
  • Welcome contributions with clear guidelines
  • Maintain consistent release schedules
  • Engage with users and gather feedback

Long-term Maintenance

  • Plan for long-term support
  • Consider deprecated feature policies
  • Maintain backward compatibility when possible
  • Have succession planning for popular packages

Publishing and maintaining a successful PHP package requires ongoing commitment to quality, community engagement, and continuous improvement. Focus on solving real problems and providing excellent developer experience to build a thriving package ecosystem.