Package Version Control
Introduction to Package Version Control
Version control is essential for PHP package development, enabling collaborative development, tracking changes, and managing releases. This guide covers Git workflows, semantic versioning, and release management strategies.
Git Fundamentals for Packages
Repository Structure
# Initialize a new package repository
git init my-package
cd my-package
# Set up initial structure
mkdir -p src tests docs
touch composer.json README.md LICENSE .gitignore
# Add initial commit
git add .
git commit -m "Initial package structure"
# Set up remote repository
git remote add origin https://github.com/vendor/my-package.git
git push -u origin main
Gitignore for PHP Packages
# .gitignore
# Dependencies
/vendor/
/node_modules/
# IDE files
/.idea/
/.vscode/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Test coverage
/coverage/
/coverage.xml
/coverage.clover
/.phpunit.cache
# Build artifacts
/build/
/dist/
# Environment files
.env
.env.local
# Logs
*.log
# Cache files
/.php-cs-fixer.cache
/.phpstan.cache
# Temporary files
/tmp/
Semantic Versioning
SemVer Principles
Semantic versioning follows the MAJOR.MINOR.PATCH
format:
- MAJOR: Incompatible API changes
- MINOR: Backward-compatible functionality additions
- PATCH: Backward-compatible bug fixes
Version Examples
# Initial release
1.0.0
# Bug fix
1.0.1
# New feature (backward compatible)
1.1.0
# Breaking change
2.0.0
# Pre-release versions
1.1.0-alpha.1
1.1.0-beta.1
1.1.0-rc.1
Composer Version Constraints
{
"require": {
"vendor/package": "^1.0", // >=1.0.0 <2.0.0
"vendor/package": "~1.2", // >=1.2.0 <1.3.0
"vendor/package": ">=1.2.0", // Minimum version
"vendor/package": "1.2.*", // Any patch level of 1.2
"vendor/package": "dev-main" // Development branch
}
}
Branching Strategies
Git Flow
# Main branches
main # Production-ready code
develop # Integration branch for features
# Supporting branches
feature/* # New features
release/* # Release preparation
hotfix/* # Emergency fixes
GitHub Flow (Simplified)
# Single main branch with feature branches
main # Always deployable
feature/* # Short-lived feature branches
Package-Specific Workflow
# Create feature branch
git checkout -b feature/new-calculator-method
# Work on feature
git add src/Calculator.php tests/CalculatorTest.php
git commit -m "Add power calculation method"
# Push feature branch
git push origin feature/new-calculator-method
# Create pull request on GitHub
# After review and approval, merge to main
# Tag release
git checkout main
git pull origin main
git tag v1.1.0
git push origin v1.1.0
Release Management
Preparing a Release
# 1. Update version in composer.json
# 2. Update CHANGELOG.md
# 3. Run tests
composer test
# 4. Check code quality
composer cs-check
composer analyze
# 5. Commit changes
git add .
git commit -m "Prepare v1.1.0 release"
# 6. Create and push tag
git tag -a v1.1.0 -m "Release version 1.1.0"
git push origin v1.1.0
Automated Release Script
<?php
// scripts/release.php
class ReleaseManager
{
private string $currentVersion;
private string $newVersion;
public function __construct()
{
$this->currentVersion = $this->getCurrentVersion();
}
public function release(string $type): void
{
$this->validateWorkingDirectory();
$this->newVersion = $this->calculateNewVersion($type);
$this->updateComposerJson();
$this->updateChangelog();
$this->runTests();
$this->commitChanges();
$this->createTag();
$this->pushChanges();
echo "Released version {$this->newVersion}\n";
}
private function getCurrentVersion(): string
{
$composer = json_decode(file_get_contents('composer.json'), true);
return $composer['version'] ?? '0.0.0';
}
private function calculateNewVersion(string $type): string
{
[$major, $minor, $patch] = explode('.', $this->currentVersion);
switch ($type) {
case 'major':
return ($major + 1) . '.0.0';
case 'minor':
return $major . '.' . ($minor + 1) . '.0';
case 'patch':
return $major . '.' . $minor . '.' . ($patch + 1);
default:
throw new InvalidArgumentException("Invalid release type: {$type}");
}
}
private function validateWorkingDirectory(): void
{
$status = shell_exec('git status --porcelain');
if (!empty(trim($status))) {
throw new RuntimeException('Working directory is not clean');
}
$branch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
if ($branch !== 'main') {
throw new RuntimeException('Not on main branch');
}
}
private function updateComposerJson(): void
{
$composer = json_decode(file_get_contents('composer.json'), true);
$composer['version'] = $this->newVersion;
file_put_contents(
'composer.json',
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
);
}
private function updateChangelog(): void
{
$date = date('Y-m-d');
$changelog = file_get_contents('CHANGELOG.md');
$newEntry = "## [{$this->newVersion}] - {$date}\n\n";
// Insert after the first ## [Unreleased] section
$changelog = preg_replace(
'/^(## \[Unreleased\].*?\n\n)/ms',
"$1{$newEntry}",
$changelog
);
file_put_contents('CHANGELOG.md', $changelog);
}
private function runTests(): void
{
$result = shell_exec('composer test 2>&1');
$exitCode = shell_exec('echo $?');
if (trim($exitCode) !== '0') {
throw new RuntimeException("Tests failed:\n{$result}");
}
}
private function commitChanges(): void
{
shell_exec('git add .');
shell_exec("git commit -m 'Release version {$this->newVersion}'");
}
private function createTag(): void
{
shell_exec("git tag -a v{$this->newVersion} -m 'Release version {$this->newVersion}'");
}
private function pushChanges(): void
{
shell_exec('git push origin main');
shell_exec("git push origin v{$this->newVersion}");
}
}
// Usage
$type = $argv[1] ?? 'patch';
$releaseManager = new ReleaseManager();
$releaseManager->release($type);
?>
Changelog Management
CHANGELOG.md Structure
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New features that will be included in the next release
### Changed
- Changes in existing functionality
### Deprecated
- Soon-to-be removed features
### Removed
- Features removed in this release
### Fixed
- Bug fixes
### Security
- Security improvements
## [1.2.0] - 2023-12-01
### Added
- Added power calculation method to Calculator class
- Added logarithm function to Math\Advanced class
- Added configuration option for calculation precision
### Changed
- Improved error messages for invalid operations
- Updated minimum PHP version to 8.0
### Fixed
- Fixed division by zero handling
- Fixed floating point precision issues
## [1.1.0] - 2023-11-15
### Added
- Added factorial calculation
- Added Fibonacci sequence generation
- Added comprehensive test coverage
### Fixed
- Fixed composer autoloader configuration
## [1.0.0] - 2023-10-01
### Added
- Initial release
- Basic calculator functionality (add, subtract, multiply, divide)
- PSR-4 autoloading
- Comprehensive documentation
[Unreleased]: https://github.com/vendor/package/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/vendor/package/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/vendor/package/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/vendor/package/releases/tag/v1.0.0
Automated Changelog Generation
<?php
// scripts/changelog-generator.php
class ChangelogGenerator
{
private string $repository;
public function __construct(string $repository)
{
$this->repository = $repository;
}
public function generateChangelog(string $fromTag, string $toTag = 'HEAD'): string
{
$commits = $this->getCommitsBetween($fromTag, $toTag);
$groupedCommits = $this->groupCommitsByType($commits);
return $this->formatChangelog($groupedCommits, $toTag);
}
private function getCommitsBetween(string $from, string $to): array
{
$command = "git log --pretty=format:'%h|%s|%an|%ad' --date=short {$from}..{$to}";
$output = shell_exec($command);
$commits = [];
foreach (explode("\n", trim($output)) as $line) {
if (empty($line)) continue;
[$hash, $subject, $author, $date] = explode('|', $line);
$commits[] = [
'hash' => $hash,
'subject' => $subject,
'author' => $author,
'date' => $date
];
}
return $commits;
}
private function groupCommitsByType(array $commits): array
{
$groups = [
'added' => [],
'changed' => [],
'deprecated' => [],
'removed' => [],
'fixed' => [],
'security' => [],
'other' => []
];
foreach ($commits as $commit) {
$type = $this->categorizeCommit($commit['subject']);
$groups[$type][] = $commit;
}
return array_filter($groups);
}
private function categorizeCommit(string $subject): string
{
$subject = strtolower($subject);
if (preg_match('/^(add|feat|feature)/', $subject)) {
return 'added';
}
if (preg_match('/^(fix|bug)/', $subject)) {
return 'fixed';
}
if (preg_match('/^(update|change|modify)/', $subject)) {
return 'changed';
}
if (preg_match('/^(remove|delete)/', $subject)) {
return 'removed';
}
if (preg_match('/^(deprecate)/', $subject)) {
return 'deprecated';
}
if (preg_match('/^(security|sec)/', $subject)) {
return 'security';
}
return 'other';
}
private function formatChangelog(array $groupedCommits, string $version): string
{
$changelog = "## [{$version}] - " . date('Y-m-d') . "\n\n";
$sectionMap = [
'added' => 'Added',
'changed' => 'Changed',
'deprecated' => 'Deprecated',
'removed' => 'Removed',
'fixed' => 'Fixed',
'security' => 'Security'
];
foreach ($sectionMap as $key => $title) {
if (isset($groupedCommits[$key]) && !empty($groupedCommits[$key])) {
$changelog .= "### {$title}\n\n";
foreach ($groupedCommits[$key] as $commit) {
$changelog .= "- {$commit['subject']} ({$commit['hash']})\n";
}
$changelog .= "\n";
}
}
return $changelog;
}
}
// Usage
$generator = new ChangelogGenerator('vendor/package-name');
$changelog = $generator->generateChangelog('v1.1.0', 'v1.2.0');
echo $changelog;
?>
GitHub Integration
GitHub Actions for Package Testing
# .github/workflows/tests.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.0, 8.1, 8.2]
dependency-version: [prefer-lowest, prefer-stable]
name: P${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: xdebug
- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install dependencies
run: |
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.clover
Release Action
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
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: Run tests
run: composer test
- name: Generate changelog
id: changelog
run: |
CHANGELOG=$(php scripts/changelog-generator.php ${{ github.ref_name }})
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- 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: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
Pull Request Workflow
PR Template
<!-- .github/pull_request_template.md -->
## Description
Brief description of the changes.
## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
## How Has This Been Tested?
- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual testing
## Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
Code Review Guidelines
<?php
// Example of well-documented code for review
class CalculatorService
{
/**
* Performs division with proper error handling
*
* @param float $dividend The number to be divided
* @param float $divisor The number to divide by
* @return float The result of the division
* @throws InvalidArgumentException When divisor is zero
*/
public function divide(float $dividend, float $divisor): float
{
// Validate input to prevent division by zero
if ($divisor === 0.0) {
throw new InvalidArgumentException('Division by zero is not allowed');
}
return $dividend / $divisor;
}
}
?>
Best Practices
Commit Message Convention
# Format: type(scope): description
# Types:
feat # New feature
fix # Bug fix
docs # Documentation changes
style # Code style changes (formatting, etc.)
refactor # Code refactoring
test # Adding or updating tests
chore # Maintenance tasks
# Examples:
feat(calculator): add power calculation method
fix(math): resolve floating point precision issue
docs(readme): update installation instructions
test(calculator): add edge case tests for division
chore(deps): update phpunit to v10
Release Notes Best Practices
- Clear categorization of changes
- Impact assessment for breaking changes
- Migration guides for major updates
- Security advisories when applicable
- Performance improvements documentation
Version Management Tips
- Use pre-release versions for testing
- Maintain LTS branches for long-term support
- Follow deprecation policies before removing features
- Provide upgrade paths for breaking changes
- Document compatibility matrices for dependencies
Effective version control and release management ensure your PHP package remains reliable, maintainable, and trustworthy for users while facilitating smooth collaboration among contributors.