Skip to content

IMAMx39/Php-framework

Repository files navigation

PHP Framework

Un framework PHP moderne inspiré de Symfony, construit from scratch.

CI PHP License


Installation

composer create-project imamx39/php-framework mon-projet
cd mon-projet

Le script d'init s'exécute automatiquement — .env copié, répertoires créés.

Configure ton .env :

APP_NAME=MonApp
DATABASE_URL=mysql://root:@127.0.0.1:3306/ma_base

Lance le serveur de développement :

composer serve
# → http://localhost:8000

Démarrage rapide

1. Créer une entité

php bin/console make:entity Product

Génère app/Entity/Product.php et app/Repository/ProductRepository.php.

2. Créer une migration

php bin/console make:migration CreateProductsTable

Édite le fichier généré dans migrations/ :

public function up(): void
{
    $this->execute('
        CREATE TABLE products (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            name       VARCHAR(255) NOT NULL,
            price      DECIMAL(10,2) NOT NULL,
            created_at VARCHAR(255)
        )
    ');
}

public function down(): void
{
    $this->execute('DROP TABLE products');
}

3. Lancer les migrations

php bin/console migrate
php bin/console migrate:status    # voir l'état
php bin/console migrate:rollback  # annuler la dernière

4. Créer un contrôleur

php bin/console make:controller Product

Génère app/Controller/ProductController.php :

#[Route('/product', name: 'product.index', methods: ['GET'])]
public function index(Request $request): Response
{
    $products = $this->container->get(ProductRepository::class)->findAll();

    return $this->render('product/index.html.twig', [
        'products' => $products,
    ]);
}

Fonctionnalités

Routing

// config/routes.php
$router->get('/products',         [ProductController::class, 'index']);
$router->post('/products',        [ProductController::class, 'store']);
$router->get('/products/{id}',    [ProductController::class, 'show']);
$router->put('/products/{id}',    [ProductController::class, 'update']);
$router->delete('/products/{id}', [ProductController::class, 'destroy']);

Via attributs PHP 8 :

#[Route('/products/{id}', name: 'product.show', methods: ['GET'])]
public function show(Request $request, int $id): Response { ... }

ORM

// Lecture
$product  = $repo->find(1);
$products = $repo->findAll();
$actifs   = $repo->findBy(['active' => 1], ['name' => 'ASC']);
$one      = $repo->findOneBy(['email' => 'a@b.com']);
$total    = $repo->count(['active' => 1]);

// Persistance
$repo->save($product);
$repo->delete($product);

// Pagination
$page = $repo->paginate(page: 1, perPage: 15);
$page->items();     // entités de la page
$page->total();     // nombre total
$page->lastPage();  // dernière page
$page->hasMore();   // page suivante ?
$page->from();      // rang du 1er élément
$page->to();        // rang du dernier

Relations

#[Entity(table: 'posts', repositoryClass: PostRepository::class)]
class Post
{
    #[ManyToOne(targetEntity: User::class, joinColumn: 'user_id')]
    private ?User $author = null;

    #[OneToMany(targetEntity: Comment::class, mappedBy: 'post_id')]
    private array $comments = [];

    #[ManyToMany(
        targetEntity: Tag::class,
        joinTable: 'post_tags',
        joinColumn: 'post_id',
        inverseJoinColumn: 'tag_id',
    )]
    private array $tags = [];
}

// Chargement explicite des relations
$post = $repo->find(1, relations: ['author', 'tags', 'comments']);

// Gestion ManyToMany
$repo->attach($post, $tag, 'tags');
$repo->detach($post, $tag, 'tags');
$repo->sync($post, [$tag1, $tag2], 'tags');

Authentication

$auth = $container->get(Auth::class);

if ($auth->attempt($email, $password)) {
    return Response::redirect('/dashboard');
}

$auth->check();   // bool — connecté ?
$auth->user();    // User|null
$auth->id();      // int|null
$auth->logout();

Middlewares

new AuthMiddleware($auth)                                         // redirige → /login
new GuestMiddleware($auth)                                        // redirige → /dashboard
new CsrfMiddleware($csrfManager)                                  // vérifie le token CSRF
new ThrottleMiddleware($limiter, maxAttempts: 5, decaySeconds: 60) // rate limiting

Formulaires

class LoginFormType extends AbstractFormType
{
    public function buildForm(FormBuilder $builder): void
    {
        $builder
            ->add('email',    'email',    ['rules' => 'required|email'])
            ->add('password', 'password', ['rules' => 'required|min:8']);
    }
}

// Dans le contrôleur
$form = $factory->create(new LoginFormType());
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    $data = $form->getData();
}

Dans Twig :

{{ form_start(form, '/login', 'POST') }}
    {{ form_row(form, 'email') }}
    {{ form_row(form, 'password') }}
    {{ csrf_field() }}
    <button type="submit">Connexion</button>
{{ form_end() }}

Types de champs disponibles : text, email, password, number, textarea, select, checkbox, hidden.


Validation

$data = Validator::make($request->all(), [
    'name'     => 'required|string|min:2|max:100',
    'email'    => 'required|email',
    'age'      => 'required|integer|min:18',
    'role'     => 'required|in:admin,user',
    'password' => 'required|min:8|confirmed',
]);

Règles disponibles : required, string, integer, numeric, boolean, email, url, min, max, between, in, not_in, confirmed, regex.


Cache

$cache = new FileCache(dirname(__DIR__) . '/var/cache');

$cache->put('key', $value, ttl: 3600);  // TTL en secondes
$cache->get('key', default: null);
$cache->has('key');
$cache->forget('key');
$cache->flush();

// Mémoïsation — exécute le callback uniquement si absent du cache
$products = $cache->remember('products.all', 600, fn() => $repo->findAll());

Mailer

$message = (new Message())
    ->from('noreply@monapp.com', 'Mon App')
    ->to($user->getEmail(), $user->getName())
    ->subject('Bienvenue !')
    ->html('<h1>Bonjour ' . $user->getName() . ' !</h1>')
    ->text('Bonjour ' . $user->getName() . ' !');

$mailer->send($message);

Configuration SMTP dans .env :

MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=xxxxx
MAIL_PASSWORD=xxxxx
MAIL_ENCRYPTION=tls

En développement, utilise NullMailer — il absorbe les envois sans les envoyer.


Serializer

$serializer = new Serializer();

// Objet → tableau / JSON
$array = $serializer->normalize($product);
$json  = $serializer->toJson($product);
$json  = $serializer->toJson($products);          // collection

// Groupes — masquer des champs selon le contexte
$public = $serializer->normalize($user, groups: ['public']);

Annoter les propriétés par groupe :

#[Column(type: 'string')]
#[SerializeGroup('admin')]          // visible uniquement pour le groupe 'admin'
private string $passwordHash;

#[Column(type: 'string')]
#[SerializeGroup('public', 'admin')] // visible pour 'public' ET 'admin'
private string $name;

Dans un contrôleur API :

return new JsonResponse($serializer->normalize($product));

File Storage

$storage = new LocalStorage(dirname(__DIR__) . '/storage/app', '/storage');

$storage->put('avatars/user-1.jpg', $imageContent);
$content = $storage->get('avatars/user-1.jpg');
$url     = $storage->url('avatars/user-1.jpg');  // → /storage/avatars/user-1.jpg
$storage->exists('avatars/user-1.jpg');
$storage->delete('avatars/user-1.jpg');

// Upload depuis un formulaire HTML
$path = $storage->putUpload($_FILES['avatar'], directory: 'avatars');

// Lister les fichiers
$files = $storage->files('avatars');
$all   = $storage->files('', recursive: true);

Rate Limiter

$limiter = new RateLimiter($cache);

// Vérification manuelle
if (!$limiter->attempt("login:{$ip}", maxAttempts: 5, decaySeconds: 60)) {
    return new Response('Trop de tentatives. Réessaie dans 1 minute.', 429);
}

$limiter->remaining('login:' . $ip, 5); // tentatives restantes
$limiter->clear('login:' . $ip);        // remettre à zéro

// Via middleware (appliqué globalement ou par route)
new ThrottleMiddleware($limiter, maxAttempts: 60, decaySeconds: 60)

EventDispatcher

$dispatcher = $container->get(EventDispatcher::class);

// S'abonner à un événement kernel
$dispatcher->on(KernelEvents::REQUEST, function (RequestEvent $event) {
    // court-circuiter avec une réponse directe
    $event->setResponse(new Response('Maintenance', 503));
});

// Événements personnalisés
$dispatcher->on('user.registered', function ($event) {
    // envoyer un email de bienvenue
}, priority: 10);

$dispatcher->emit('user.registered', new Event());

Logger (PSR-3)

$logger = $container->get(Logger::class);

$logger->info('Utilisateur connecté', ['user_id' => 42]);
$logger->warning('Tentative échouée', ['ip' => $ip]);
$logger->error('Erreur critique', ['exception' => $e]);

Niveaux disponibles (RFC 5424) : emergency alert critical error warning notice info debug.

Logs écrits dans var/logs/app.log et var/logs/error.log.


CSRF Protection

// Middleware global
$pipeline->pipe(new CsrfMiddleware($csrfManager));

// Avec exemptions (webhooks, API)
new CsrfMiddleware($csrfManager, exemptPaths: ['/api/', '/webhook/'])

Dans Twig :

<form method="POST">
    {{ csrf_field() }}
</form>

Pour les requêtes AJAX :

fetch('/api/data', {
    method: 'POST',
    headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
});

Console

# Génération de code
php bin/console make:entity     Product
php bin/console make:migration  CreateProductsTable
php bin/console make:controller Product

# Migrations
php bin/console migrate
php bin/console migrate:status
php bin/console migrate:rollback

Structure du projet

mon-projet/
├── app/
│   ├── Controller/        # Contrôleurs de l'application
│   ├── Entity/            # Entités ORM
│   └── Repository/        # Repositories
├── bin/
│   ├── console            # CLI du framework
│   └── setup              # Script d'initialisation
├── config/
│   ├── routes.php         # Définition des routes
│   └── services.php       # Conteneur de services
├── migrations/            # Fichiers de migration versionnés
├── public/
│   └── index.php          # Point d'entrée HTTP
├── src/                   # Code source du framework
├── templates/             # Templates Twig
├── tests/                 # Tests PHPUnit
├── var/
│   ├── cache/             # Cache (auto-généré)
│   └── logs/              # Logs (auto-généré)
├── storage/
│   └── app/               # Fichiers uploadés
├── .env                   # Configuration locale (ignoré par git)
├── .env.example           # Template de configuration
└── composer.json

Tests

composer test
# ou
vendor/bin/phpunit --testdox

354 tests · 583 assertions — tout vert sur PHP 8.1 / 8.2 / 8.3 / 8.4.


Licence

MIT — IMAMx39

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors