
php-framework-development-skills
by cedpaqlab
PHP 8.3+ framework PoC featuring Propel ORM 2.0, database migrations, role-based auth, CSRF protection, and comprehensive test suite. Experimental AI-generated project.
SKILL.md
name: PHP Framework Development Skills description: Collection of reusable skills for developing components in the Nexus PHP Framework
Framework Development Skills
Skill 1: Create PHP Controller
When creating a new Controller:
-
Location: Place in
app/Http/Controllers/orapp/Http/Controllers/{Domain}/following domain structure -
Structure:
- Use constructor injection for dependencies (ViewRenderer, Response, Services, Repositories)
- Methods receive
Request $requestas first parameter - Return
Responseinstance - Use fluent Response methods (json, html, redirect, etc.)
-
Example Pattern:
<?php
declare(strict_types=1);
namespace App\Http\Controllers\{Domain};
use App\Http\Request;
use App\Http\Response;
use App\Services\View\ViewRenderer;
use App\Services\{Domain}\{Name}Service;
class {Name}Controller
{
public function __construct(
private ViewRenderer $viewRenderer,
private Response $response,
private {Name}Service $service
) {
}
public function index(Request $request): Response
{
$data = $this->service->getAll();
return $this->response->json(['data' => $data]);
}
}
- Register in Container: Add binding in
app/Providers/AppServiceProvider.phpif needed - Add Route: Define route in
routes/web.php - Create Tests: Add corresponding test in
tests/framework/Http/Controllers/
Skill 2: Create Domain Service
When creating a new Service:
-
Location: Place in
app/Services/{Domain}/following domain structure -
Structure:
- Single Responsibility Principle (one reason to change)
- Constructor injection for dependencies (Repositories, other Services, Logger)
- Use typed exceptions for error handling
- Methods should be focused and testable
-
Example Pattern:
<?php
declare(strict_types=1);
namespace App\Services\{Domain};
use App\Repositories\{Domain}\{Name}Repository;
use App\Services\Logger\Logger;
use App\Exceptions\{Domain}\{Name}Exception;
class {Name}Service
{
public function __construct(
private {Name}Repository $repository,
private Logger $logger
) {
}
public function doSomething(string $param): mixed
{
// Implementation with validation, logging, error handling
return $result;
}
}
- Register in Container: Add singleton binding in
app/Providers/AppServiceProvider.php - Create Tests: Add corresponding test in
tests/framework/Services/{Domain}/
Skill 3: Create Repository with Propel ORM
CRITICAL: Framework uses 100% Propel ORM. Never use QueryBuilder or raw SQL for data operations.
When creating a new Repository:
-
Location: Place in
app/Repositories/{Domain}/following domain structure -
Structure:
- Inject
PropelConnector(or domain-specific connector) via constructor - All data operations go through Propel models
- Use
executeInTransaction()for multi-step operations - Convert Propel models to arrays using
toArray()helper method - Never use QueryBuilder or raw SQL for data access
- Inject
-
Example Pattern:
<?php
declare(strict_types=1);
namespace App\Repositories\{Domain};
use App\Repositories\Connectors\{Name}Connector;
use App\Models\{Name};
class {Name}Repository
{
public function __construct(
private {Name}Connector $connector
) {
}
public function findById(int $id): ?array
{
$model = $this->connector->find{Name}ById($id);
return $model ? $this->toArray($model) : null;
}
public function findAll(array $conditions = [], array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{
$models = $this->connector->findAll{Name}s($conditions, $orderBy, $limit, $offset);
return array_map([$this, 'toArray'], $models);
}
public function create(array $data): int
{
return $this->connector->executeInTransaction(function () use ($data) {
$model = $this->connector->create{Name}($data);
return $model->getId();
});
}
public function update(int $id, array $data): int
{
return $this->connector->executeInTransaction(function () use ($id, $data) {
$model = $this->get{Name}OrFail($id);
$this->connector->update{Name}($model, $data);
return 1;
});
}
public function delete(int $id): int
{
return $this->connector->executeInTransaction(function () use ($id) {
$model = $this->get{Name}OrFail($id);
$this->connector->delete{Name}($model);
return 1;
});
}
private function get{Name}OrFail(int $id): {Name}
{
$model = $this->connector->find{Name}ById($id);
if ($model === null) {
throw new \RuntimeException("{Name} with ID {$id} not found");
}
return $model;
}
private function toArray({Name} $model): array
{
return [
'id' => $model->getId(),
// Map all properties
'created_at' => $model->getCreatedAt()?->format('Y-m-d H:i:s'),
'updated_at' => $model->getUpdatedAt()?->format('Y-m-d H:i:s'),
];
}
}
- Create Tests: Add corresponding test in
tests/framework/Repositories/{Domain}/
Skill 4: Create Propel Connector
When creating a new Propel Connector:
-
Location: Place in
app/Repositories/Connectors/ -
Structure:
- Initialize Propel in constructor via
PropelInitializer::initialize() - Use Propel Query classes (e.g.,
UserQuery,ProductQuery) - Use Propel Model classes (e.g.,
User,Product) - Wrap queries in
executeQuery()for error handling - Use
executeInTransaction()for write operations - Never use raw SQL or QueryBuilder
- Initialize Propel in constructor via
-
Example Pattern:
<?php
declare(strict_types=1);
namespace App\Repositories\Connectors;
use App\Models\{Name};
use App\Models\{Name}Query;
use App\Repositories\Connectors\PropelInitializer;
use Propel\Runtime\Propel;
use Propel\Runtime\Exception\PropelException;
class {Name}Connector
{
public function __construct()
{
PropelInitializer::initialize();
}
public function find{Name}ById(int $id): ?{Name}
{
return $this->executeQuery(fn() => {Name}Query::create()->findPk($id));
}
public function find{Name}By{Field}(string $value): ?{Name}
{
return $this->executeQuery(fn() => {Name}Query::create()->findOneBy{Field}($value));
}
public function findAll{Name}s(array $conditions = [], array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{
return $this->executeQuery(
fn() => $this->buildQuery($conditions, $orderBy, $limit, $offset)->find()->getData(),
[]
);
}
public function create{Name}(array $data): {Name}
{
$model = new {Name}();
$model->set{Field}($data['field']);
// Set all fields
$model->save();
return $model;
}
public function update{Name}({Name} $model, array $data): {Name}
{
if (isset($data['field'])) {
$model->set{Field}($data['field']);
}
$model->save();
return $model;
}
public function delete{Name}({Name} $model): void
{
$model->delete();
}
public function executeInTransaction(callable $callback): mixed
{
$connection = Propel::getConnection();
$connection->beginTransaction();
try {
$result = $callback();
$connection->commit();
return $result;
} catch (\Exception $e) {
$connection->rollBack();
throw $e;
}
}
private function buildQuery(array $conditions, array $orderBy, ?int $limit, ?int $offset): {Name}Query
{
$query = {Name}Query::create();
foreach ($conditions as $field => $value) {
$method = 'filterBy' . ucfirst($field);
if (method_exists($query, $method)) {
$query->$method($value);
}
}
foreach ($orderBy as $field => $direction) {
$method = 'orderBy' . ucfirst($field);
if (method_exists($query, $method)) {
$query->$method($direction);
}
}
if ($limit !== null) {
$query->limit($limit);
}
if ($offset !== null) {
$query->offset($offset);
}
return $query;
}
private function executeQuery(callable $callback, mixed $default = null): mixed
{
try {
return $callback();
} catch (PropelException $e) {
error_log("Propel error: " . $e->getMessage());
return $default;
}
}
}
- Create Tests: Add corresponding test in
tests/framework/Repositories/Connectors/
Skill 5: Use Propel ORM Models
When working with Propel models:
- Always use Propel Query classes for reads:
use App\Models\User;
use App\Models\UserQuery;
// Find by primary key
$user = UserQuery::create()->findPk(1);
// Find by unique field
$user = UserQuery::create()->findOneByEmail('user@example.com');
// Filter and order
$admins = UserQuery::create()
->filterByRole('admin')
->orderByCreatedAt('DESC')
->find();
// Count
$count = UserQuery::create()->filterByRole('user')->count();
- Always use Propel Model classes for writes:
// Create
$user = new User();
$user->setEmail('new@example.com');
$user->setPassword($hashedPassword);
$user->setName('New User');
$user->save();
// Update
$user = UserQuery::create()->findPk(1);
$user->setName('Updated Name');
$user->save();
// Delete
$user = UserQuery::create()->findPk(1);
$user->delete();
- Never use raw SQL or QueryBuilder for data operations
- Use
getData()on collections, nottoArray()(returns Collection of models)
Skill 6: Create Request Validation
When creating a new Request validation:
-
Location: Place in
app/Http/Requests/{Domain}/following domain structure -
Structure:
- Extend
BaseRequest - Implement
rules()method returning validation rules array - Use
validated()method to get validated data - Validation errors automatically return 422 Response
- Use whitelist approach (only allow specified fields)
- Extend
-
Example Pattern:
<?php
declare(strict_types=1);
namespace App\Http\Requests\{Domain};
use App\Http\Requests\BaseRequest;
class {Name}Request extends BaseRequest
{
protected function rules(): array
{
return [
'field1' => ['required', 'string', 'min:3', 'max:255'],
'field2' => ['required', 'email'],
'field3' => ['numeric', 'min:0'],
'field4' => ['required', 'in:value1,value2'],
];
}
}
- Usage in Controller:
$request = new {Name}Request($request, $validator, $response);
$data = $request->validated(); // Returns array or sends 422 Response
- Create Tests: Add corresponding test in
tests/framework/Http/Requests/{Domain}/
Skill 7: Create Middleware
When creating a new Middleware:
-
Location: Place in
app/Http/Middlewares/ -
Structure:
- Implement
MiddlewareInterface - Inject
Responsevia constructor handle()method receivesRequestandcallable $next- Return
Responsefrom$next($request)or error response
- Implement
-
Example Pattern:
<?php
declare(strict_types=1);
namespace App\Http\Middlewares;
use App\Http\Request;
use App\Http\Response;
use App\Http\Middlewares\MiddlewareInterface;
class {Name}Middleware implements MiddlewareInterface
{
public function __construct(
private Response $response
) {
}
public function handle(Request $request, callable $next): Response
{
// Pre-processing logic
if (!$this->shouldProceed($request)) {
return $this->response->forbidden('Access denied');
}
$response = $next($request);
// Post-processing logic (optional)
return $response;
}
private function shouldProceed(Request $request): bool
{
// Validation logic
return true;
}
}
- Register in Router: Use in route definitions or route groups in
routes/web.php - Create Tests: Add corresponding test in
tests/framework/Http/Middlewares/
Skill 8: Create Database Migration
When creating a new migration:
-
Location: Place in
database/migrations/ -
Naming:
{timestamp}_{description}.php(e.g.,20240114120000_create_users_table.php) -
Structure:
- Implement
MigrationInterface - Use direct PDO via
Connection::getInstance()for DDL operations - Include both
up()anddown()methods - Use raw SQL for CREATE/ALTER/DROP (DDL operations)
- Add proper indexes and foreign keys
- Use transactions for multi-step operations
- Implement
-
Example Pattern:
<?php
declare(strict_types=1);
namespace Database\Migrations;
use App\Database\Migrations\MigrationInterface;
use App\Repositories\Database\Connection;
use PDO;
class {Name}Migration implements MigrationInterface
{
private PDO $pdo;
public function __construct()
{
$this->pdo = Connection::getInstance();
}
public function up(): void
{
$this->pdo->exec("
CREATE TABLE {table_name} (
id INT AUTO_INCREMENT PRIMARY KEY,
column1 VARCHAR(255) NOT NULL,
column2 INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_column1 (column1)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
public function down(): void
{
$this->pdo->exec("DROP TABLE IF EXISTS {table_name}");
}
}
- Security: Migrations use DDL (CREATE/ALTER/DROP) - no user input involved
- Create Tests: Add corresponding test in
tests/framework/Database/Migrations/
Skill 9: Create Database Seeder
When creating a new seeder:
-
Location: Place in
database/seeders/ -
Naming:
{timestamp}_{description}.php(e.g.,20240101000000_DefaultUsersSeeder.php) -
Structure:
- Implement
SeederInterface - Use
PropelConnector(or domain-specific connector) for all data operations - Check for existing records before creating (update if exists)
- Use Propel models, never raw SQL
- Implement
-
Example Pattern:
<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Database\Seeders\SeederInterface;
use App\Repositories\Connectors\PropelConnector;
use App\Services\Security\HashService;
class {Name}Seeder implements SeederInterface
{
private HashService $hashService;
public function __construct()
{
$this->hashService = new HashService();
}
public function run(PropelConnector $connector): void
{
$items = [
[
'field1' => 'value1',
'field2' => 'value2',
],
];
foreach ($items as $itemData) {
$existing = $connector->find{Name}By{Field}($itemData['field']);
if ($existing === null) {
$connector->create{Name}($itemData);
} else {
// Update existing record
$connector->update{Name}($existing, $itemData);
}
}
}
}
- Create Tests: Add corresponding test in
tests/framework/Database/Seeders/
Skill 10: Modify Propel Schema
When modifying the database schema:
-
Location: Edit
schema.xmlat project root -
Structure:
- Use Propel XML schema format
- Use
LONGVARCHARinstead ofTEXTfor compatibility - Define foreign keys and relationships
- Use ENUM types for constrained values
-
After modifying schema.xml:
# Generate Propel configuration
vendor/bin/propel config:convert
# Generate models
vendor/bin/propel model:build --schema-dir=. --output-dir=app
- Important:
- Generated files in
app/Models/Base/are auto-generated - manual fixes will be overwritten - If ENUM handling needs fixes, re-apply after each
model:build - Create migration for schema changes if needed
- Generated files in
Skill 11: Create PHPUnit Test
When creating a new test:
-
Location: Place in
tests/framework/{Category}/matching app structure -
Structure:
- Extend
Tests\Support\TestCase - Use
setUp()andtearDown()for test isolation - Use database transactions or fresh database for each test
- Follow AAA pattern (Arrange, Act, Assert)
- Extend
-
Example Pattern:
<?php
declare(strict_types=1);
namespace Tests\Framework\{Category};
use Tests\Support\TestCase;
use App\{Category}\{Name};
class {Name}Test extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Setup test data
}
public function testSomething(): void
{
// Arrange
$input = 'value';
// Act
$result = $this->subject->method($input);
// Assert
$this->assertEquals('expected', $result);
}
}
- Run tests:
vendor/bin/phpunitorcomposer test
Key Principles
-
100% Propel ORM: Never use QueryBuilder or raw SQL for data operations. Only use direct PDO for DDL in migrations.
-
Architecture: Controller → Service → Repository → Connector (Propel) → Database
-
SOLID Principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
-
Security:
- CSRF protection on all forms
- Input validation with whitelist approach
- Password hashing (bcrypt)
- SQL injection protection (Propel ORM)
- XSS protection (input sanitization)
-
Code Quality: DRY, KISS, YAGNI, "Sur la coche" (all quality rules applied)
-
Testing: Comprehensive test coverage, TDD when appropriate
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon