phpkg Concepts

Understanding how phpkg works under the hood.


Core Philosophy

phpkg is built on three core principles:

  1. Git-First: Direct Git repository access, no central registry
  2. Function-First: Autoload namespaced functions alongside classes
  3. Build-First: Explicit build step creates optimized, portable artifacts

How phpkg Works

1. Package Management

Package Identification

phpkg supports multiple ways to identify packages:

Simplified Formats:
  - owner/repo     → GitHub packages
  - repo           → php-repos packages (assumes php-repos/owner)

Full URLs:
  - https://github.com/owner/repo.git
  - git@github.com:owner/repo.git

Package Resolution

When you run phpkg add owner/repo:

  1. Parse identifier: Determines if it's simplified or full URL
  2. Resolve URL: Converts simplified format to full Git URL
  3. Fetch metadata: Queries Git repository for tags/releases
  4. Select version: Chooses version (latest, specific, or development)
  5. Download: Clones repository to Packages/owner/repo/
  6. Read config: Parses package's phpkg.config.json (if exists)
  7. Resolve dependencies: Uses CSP to find possible solutions, then SAT to select the best (lowest high version)
  8. Update config: Writes to phpkg.config.json and phpkg.config-lock.json

Version Selection

  • Latest: Fetches highest semantic version tag (when version is omitted)
  • Specific: Uses exact tag (e.g., v1.2.3) - must be the complete version number
  • Development: Locks to the latest commit hash at the time of adding/updating (no tag required). When updating with development, it moves to the newest commit hash.

2. Autoloading System

Namespace Mapping

phpkg uses namespace-to-directory mapping:

{
  "map": {
    "App": "src",
    "App\\Utils": "src/utils"
  }
}

This means:

  • App\ namespace → src/ directory
  • App\Utils\ namespace → src/utils/ directory

Function Autoloading

Unlike Composer (PSR-4), phpkg autoloads both classes and functions:

// src/Utils.php
namespace App\Utils;

// This function is autoloaded!
function format($text) {
    return strtoupper($text);
}

// This class is also autoloaded
class Formatter {
    // ...
}

How it works:

  1. phpkg scans mapped directories
  2. Finds all PHP files
  3. Extracts namespace declarations
  4. Adds require_once statements for functions and constants directly in files where they're used
  5. Generates autoload file with class mappings
  6. Includes the import file in entry points

How Functions, Constants, and Classes Are Handled

Functions and Constants: require_once statements are added directly in the files where they are used. For example, if you use use function App\Utils\format; in src/Controller.php, phpkg adds require_once '/path/to/src/Utils.php'; directly in that file.

Classes: Autoloading is set up in the import file (phpkg.imports.php or custom import-file):

<?php
// Auto-generated by phpkg

// Classes only - functions and constants are handled inline
spl_autoload_register(function ($class) {
    if ($class === 'App\Formatter') {
        require_once __DIR__ . '/src/Formatter.php';
    }
    // ... more class mappings
});

This import file is automatically included in all entry points.

3. Build System

Build Process

When you run phpkg build:

  1. Read config: Loads phpkg.config.json
  2. Resolve packages: Reads installed packages from phpkg.config-lock.json
  3. Create build directory: build/
  4. Copy files: Copies source files respecting excludes
  5. Copy packages: Copies package files to build
  6. Add function/constant requires: Adds require_once statements directly in files where functions and constants are used
  7. Generate class autoload: Creates phpkg.imports.php with class mappings
  8. Inject imports: Adds require statement for the import file to all entry points
  9. Create executables: Creates symlinks for executables (Unix) or copies (Windows)

Build Output

Build (phpkg build):

  • Creates build/ directory
  • Includes all source files (respecting excludes)
  • Preserves directory structure
  • Copies installed packages
  • Generates autoload file
  • Injects autoloading into entry points

Entry Point Injection

phpkg automatically injects autoloading into entry points:

Before build (public/index.php):

<?php
echo "Hello World";

After build (build/public/index.php):

<?php
require __DIR__ . '/../phpkg.imports.php';
echo "Hello World";

4. Dependency Resolution

How Dependency Resolution Works

phpkg uses a two-step process to resolve dependencies:

  1. CSP (Constraint Satisfaction Problem): Finds all possible solutions that satisfy the dependency requirements
  2. SAT (Boolean Satisfiability Problem): Selects the best solution from the possible options

Version Selection Strategy

When resolving dependencies, phpkg:

  • Always selects the lowest high version that satisfies all requirements
  • Does not use wildcards like Composer (no ^, ~, >=, etc.)
  • Requires exact version specifications or uses the latest available version

Lock File

phpkg.config-lock.json stores:

  • Exact versions installed
  • Commit hashes
  • Content checksums
  • Owner and repository information

This ensures reproducible builds by locking exact package versions and commit hashes.

5. Standalone Execution

Run Command

phpkg run executes packages without installation:

  1. Download: Clones repository to temp directory
  2. Build: Creates temporary build
  3. Execute: Runs entry point
  4. Cleanup: Removes temp files (on reboot)

Use cases:

  • CLI tools (no project needed)
  • One-off scripts
  • Testing packages

Serve Command

phpkg serve runs packages as web apps:

  1. Download: Clones repository
  2. Build: Creates temporary build
  3. Start server: Runs php -S localhost:8000
  4. Handle signals: Graceful shutdown on Ctrl+C
  5. Cleanup: Removes temp files

Use cases:

  • Web dashboards
  • Local testing servers
  • Demo applications

File Structure

Project Structure

my-project/
├── phpkg.config.json          # Project configuration
├── phpkg.config-lock.json    # Locked versions
├── src/                       # Source code
│   └── App/
│       └── Utils.php
├── Packages/                  # Installed packages
│   └── php-repos/
│       └── observer/
└── build/                     # Build outputs
    ├── phpkg.imports.php
    └── src/
└── public/                    # Entry points
    └── index.php

Config File Structure

{
  "map": {
    "App": "src"
  },
  "autoloads": ["src/helpers.php"],
  "excludes": ["node_modules"],
  "entry-points": ["public/index.php"],
  "executables": {
    "my-tool": "Packages/tool/run.php"
  },
  "import-file": "phpkg.imports.php",
  "packages-directory": "Packages",
  "packages": {
    "https://github.com/owner/repo.git": "v1.0.0"
  },
  "aliases": {
    "observer": "https://github.com/php-repos/observer.git"
  }
}

Comparison with Composer

Aspect phpkg Composer
Registry None (Git direct) Packagist (central)
Function Autoloading ✅ Yes ❌ No (PSR-4 classes only)
Build Step ✅ Required ❌ Not needed
Standalone Execution ✅ Yes (run, serve) ❌ No
Lock File phpkg.config-lock.json composer.lock
Config File phpkg.config.json composer.json
Vendor Directory Packages/ (customizable) vendor/ (fixed)
Dependency Resolution CSP + SAT solvers (lowest high version) Semantic versioning with wildcards

Workflow Patterns

Active Development Workflow

1. phpkg init              # Initialize project
2. phpkg add packages      # Add dependencies
3. phpkg build             # Build project
4. phpkg watch             # Auto-rebuild on changes
5. Edit code               # Make changes
6. phpkg build             # Rebuild before deployment

Standalone Tool Workflow

1. phpkg run tool          # Run without installation
2. Or: phpkg serve app     # Serve as web app

Team Collaboration Workflow

1. Developer A: phpkg add package
2. Commit phpkg.config.json and phpkg.config-lock.json
3. Developer B: git pull
4. Developer B: phpkg install  # Gets same versions

Advanced Concepts

Package Configuration

Packages can have their own phpkg.config.json:

{
  "map": {
    "Package": "src"
  },
  "entry-points": ["bin/console"],
  "executables": {
    "console": "bin/console"
  }
}

This allows packages to:

  • Define their own namespace mappings
  • Specify entry points
  • Create executables

Package Dependencies

When a package declares dependencies in its phpkg.config.json, phpkg uses CSP and SAT solvers to resolve them:

  • CSP solver: Finds all possible version combinations that satisfy requirements
  • SAT solver: Selects the optimal solution (lowest high version that satisfies all constraints)
  • Dependencies are automatically resolved and installed when you add a package

Build Optimization

You can optimize builds by:

  • Excluding unnecessary files in excludes config
  • Excluding test files, documentation, etc.
  • Using excludes to reduce build size

Understanding the Magic

Why Function Autoloading?

PHP's autoloading (PSR-4) only works for classes. phpkg extends this to functions by:

  1. Scanning: Finding all functions in namespaced files
  2. Mapping: Creating function-to-file mappings
  3. Lazy loading: Loading functions on first use
  4. Conflict detection: Warning on function name conflicts

Why Git-First?

  • No registry: No single point of failure
  • Direct access: Faster, more reliable
  • Version control: Git tags provide versioning
  • Flexibility: Works with any Git host

Why Build Step?

  • Optimization: Creates optimized, production-ready files
  • Portability: Build artifacts are self-contained
  • Performance: Pre-generated autoloads are faster
  • Control: Explicit control over what's included

Next Steps

Share: