- PHP 100%
- All tests now use require_exercise() for path resolution - Works in both user projects and track development - Compatible with track subdirectory structure |
||
|---|---|---|
| exercises | ||
| src | ||
| tests | ||
| .gitignore | ||
| composer.json | ||
| LICENSE | ||
| README.md | ||
Phpling Example Track
This is a template repository for creating your own Phpling tracks.
Use this as a starting point to create learning exercises for any framework, library, or programming topic.
What's Included
This example track contains:
- ✅ 5 sample exercises with detailed inline comments
- ✅ Corresponding test files with testing patterns
- ✅ Fully documented
TrackProviderimplementation - ✅ Complete
composer.jsonconfiguration - ✅ Best practices and tips for exercise design
How to Use This Template
Quick Start
-
Copy this directory
cp -r example-track my-awesome-track cd my-awesome-track -
Update
composer.json- Change
nametoyourname/phpling-yourtrack - Update
description - Change namespace from
Phpling\\ExampleTracktoYourName\\PhplingYourTrack - Update the provider class name
- Change
-
Update
src/ExampleTrackProvider.php- Rename file to match your track (e.g.,
LaravelTrackProvider.php) - Update namespace
- Change
getName()to return your track identifier (e.g., 'laravel') - Update
getDescription()
- Rename file to match your track (e.g.,
-
Install dependencies
composer install -
Create your exercises using the dev tools
# Delete the example exercises rm exercises/*.php tests/exercises/*.php # Create your first exercise using the admin commands # (available because core-dev is installed as dev dependency) ./vendor/bin/phpling add 1 "Hello World" # Edit the generated files vim exercises/001_hello_world.php vim tests/exercises/Exercise001Test.php # Test it works ./vendor/bin/phpling check 1 -
Test locally
# In a separate directory composer init composer config repositories.local path ../my-awesome-track composer require yourname/phpling-yourtrack -
Publish to Packagist
- Push to GitHub
- Submit to packagist.org
File Structure
example-track/
├── src/
│ └── ExampleTrackProvider.php # Implements TrackProvider interface
├── exercises/
│ ├── 001_hello.php # Exercise files
│ ├── 002_variables.php
│ └── ...
├── tests/
│ └── exercises/
│ ├── Exercise001Test.php # Test files
│ ├── Exercise002Test.php
│ └── ...
├── composer.json # Package configuration
└── README.md # This file
Exercise Design Guidelines
Naming Convention
Exercise files: NNN_topic_name.php
- NNN = 3-digit number (001, 002, etc.)
- topic_name = descriptive, lowercase with underscores
- Examples:
001_hello.php,015_route_parameters.php
Test files: ExerciseNNNTest.php
- Must match the exercise number
- Examples:
Exercise001Test.php,Exercise015Test.php
Writing Good Exercises
-
One Concept Per Exercise
// ❌ BAD - Too many concepts // Exercise: Create a class with inheritance and implement an interface // ✅ GOOD - One concept // Exercise: Create a simple class with properties -
Progressive Difficulty
- Exercise 001: Hello World
- Exercise 002: Variables (uses echo from 001)
- Exercise 003: Functions (uses variables from 002)
-
Clear Instructions
// ❌ BAD // Fix this code // ✅ GOOD // Create a function called greet() that: // - Takes one parameter: $name // - Returns "Hello, " followed by the name // Example: greet("Alice") returns "Hello, Alice" -
Use ??? for Missing Code
function greet(???) { return ???; } -
Include Helpful Comments
- Explain the concept being taught
- Show syntax examples
- Provide hints without giving away the answer
Writing Good Tests
-
Test Behavior, Not Implementation
// ❌ BAD - Tests implementation details test('uses a for loop', function () { ... }); // ✅ GOOD - Tests the result test('returns array with correct values', function () { ... }); -
Test Multiple Cases
test('greet function works with different names', function () { expect(greet('Alice'))->toBe('Hello, Alice!'); expect(greet('Bob'))->toBe('Hello, Bob!'); }); -
Test Edge Cases
test('handles boundary conditions', function () { expect(checkAge(17))->toBe('Minor'); expect(checkAge(18))->toBe('Adult'); // Boundary expect(checkAge(19))->toBe('Adult'); }); -
Use Descriptive Test Names
// ❌ BAD test('test 1', function () { ... }); // ✅ GOOD test('greet function returns correct greeting format', function () { ... });
Common Testing Patterns
Testing Output (echo/print)
test('outputs correct text', function () {
ob_start();
require __DIR__ . '/../../exercises/001_hello.php';
$output = ob_get_clean();
expect($output)->toBe('Hello, World!');
});
Testing Return Values
test('function returns correct value', function () {
require_once __DIR__ . '/../../exercises/003_functions.php';
expect(myFunction('input'))->toBe('expected output');
});
Testing Arrays
test('returns array with correct structure', function () {
$result = require __DIR__ . '/../../exercises/005_arrays.php';
expect($result)->toBeArray();
expect($result)->toHaveCount(3);
expect($result[0])->toBe('expected value');
});
Testing Exceptions
test('throws exception for invalid input', function () {
require_once __DIR__ . '/../../exercises/010_errors.php';
expect(fn() => riskyFunction())->toThrow(InvalidArgumentException::class);
});
Tips for Different Track Types
Framework Tracks (Laravel, Symfony, etc.)
- Focus on framework-specific features
- Use realistic examples from the framework
- Consider including boilerplate code to reduce setup
- Test using framework testing utilities
Example:
// Exercise: Creating a Route
use Illuminate\Support\Facades\Route;
Route::???(???, function () {
return ???;
});
Library Tracks (Testing, ORMs, etc.)
- Show library-specific patterns
- Demonstrate best practices
- Compare with non-library approaches
Concept Tracks (Design Patterns, Security, etc.)
- Use generic, relatable examples
- Show when and why to use the concept
- Include anti-patterns (what NOT to do)
TrackProvider Interface
Your TrackProvider must implement these methods:
public function getName(): string
// Returns: unique track identifier (e.g., 'laravel')
public function getDescription(): string
// Returns: human-readable description
public function getExercisesPath(): string
// Returns: absolute path to exercises directory
public function getTestsPath(): string
// Returns: absolute path to tests directory
public function getExercisesToCopy(): array
// Returns: ['source_path' => 'destination_path'] mapping
Advanced: Custom Installation Steps
If your track needs special setup (config files, directories, etc.):
// In composer.json
{
"scripts": {
"post-install-cmd": [
"YourName\\PhplingYourTrack\\Installer::setup"
]
}
}
// src/Installer.php
namespace YourName\PhplingYourTrack;
use Composer\Script\Event;
class Installer
{
public static function setup(Event $event): void
{
$io = $event->getIO();
// Create directories
if (!is_dir('config')) {
mkdir('config', 0755, true);
}
// Copy config files
copy(__DIR__ . '/../stubs/config.php', 'config/yourtrack.php');
$io->write('<info>Track setup complete!</info>');
}
}
Testing Your Track Locally
Before publishing:
# Create a test project
mkdir test-project && cd test-project
# Configure composer to use your local track
composer init --no-interaction
composer config repositories.my-track path ../my-awesome-track
composer require yourname/phpling-yourtrack:@dev
# Test it works
ls exercises/ # Should contain your exercises
Publishing Checklist
- Updated all names/namespaces in code
- Deleted example exercises (or kept as reference)
- Created your own exercises
- Written tests for all exercises
- Tested locally using path repository
- Added LICENSE file
- Updated this README for your track
- Created GitHub repository
- Tagged a release (v1.0.0)
- Submitted to Packagist
Questions?
- See the main CREATING_TRACKS.md for more detailed guidance
- Check out php-track for a complete real-world example
- Open an issue in the main Phpling repository
License
MIT
Happy teaching! 🚀
Replace this entire README with documentation specific to your track once you've customized it.