Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,50 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "en-US"
tone_instructions: "chill"
reviews:
profile: "chill"
high_level_summary: true
collapse_walkthrough: true
suggested_labels: false
high_level_summary_in_walkthrough: false
poem: false
path_instructions:
- path: "src/Domain/**"
instructions: |
You are reviewing PHP domain-layer code. Enforce strict domain purity:
- ❌ Do not allow infrastructure persistence side effects here.
- Flag ANY usage of Doctrine persistence APIs, especially:
- $entityManager->flush(...), $this->entityManager->flush(...)
- $em->persist(...), $em->remove(...)
- direct transaction control ($em->beginTransaction(), commit(), rollback())
- If found, request moving these calls to application-layer Command handlers or background Jobs.
- Also flag repositories in Domain that invoke flush/transactional logic; Domain repositories should be abstractions without side effects.
- Encourage domain events/outbox or return-values to signal write-intent, leaving orchestration to Commands/Jobs.

- path: "src/**/Command/**"
instructions: |
Application layer (Commands/Handlers) is the right place to coordinate persistence.
- ✅ It is acceptable to call $entityManager->flush() here.
- Check that flush is used atomically (once per unit of work) after all domain operations.
- Ensure no domain entity or domain service is calling flush; only the handler orchestrates it.
- Prefer $em->transactional(...) or explicit try/catch with rollback on failure.

- path: "src/**/MessageHandler/**"
instructions: |
Background jobs/workers may perform persistence.
- ✅ Allow $entityManager->flush() here when the job is the orchestration boundary.
- Verify idempotency and that flush frequency is appropriate (batching where practical).
- Ensure no domain-layer code invoked by the job performs flush/transaction control.

auto_review:
enabled: true
base_branches:
- ".*"
drafts: false
# ignore_title_keywords:
# - ''

#knowledge_base:
# code_guidelines:
# filePatterns:
# - ".github/AGENT.md"
103 changes: 103 additions & 0 deletions .github/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# AGENT.md — Code Review Knowledge Base for phpList/core

## 🧭 Repository Overview

This repository is the **core package** of **phpList 4**, a modular and extensible email campaign management system.

- **Purpose:** Provides the reusable foundation and framework for phpList applications and modules.
- **Consumers:** `phpList/web-frontend`, `phpList/rest-api`, and `phpList/base-distribution`.
- **Core responsibilities:**
- Application bootstrapping and service configuration
- Database ORM (Doctrine) integration
- Command-line utilities (Symfony Console)
- Async email sending via Symfony Messenger
- Logging, configuration, routing, dependency injection
- Schema definition and updates

> **Note:** This repository does *not* contain UI or REST endpoints. Those are part of other phpList packages.

---

## ⚙️ Tech Stack

| Category | Technology |
|-----------|-------------|
| Language | PHP ≥ 8.1 |
| Framework | Symfony 6.x components |
| ORM | Doctrine ORM 3.x |
| Async / Queue | Symfony Messenger |
| Tests | PHPUnit |
| Static analysis | PHPStan |
| Docs | PHPDocumentor |
| License | AGPL-3.0 |

---

## 📁 Project Structure
- .github/ CI/CD and PR templates
- bin/ Symfony console entrypoints
- config/ Application & environment configs
- docs/ Developer documentation and generated API docs
- public/ Public entrypoint for local web server
- resources/Database/ Canonical SQL schema
- src/ Core PHP source code
- tests/ PHPUnit test suites
- composer.json Package metadata and dependencies
- phpunit.xml.dist Test configuration
- phpstan.neon Static analysis configuration

---

## 💡 Code Design Principles

1. **Modularity:**
The core remains framework-like — decoupled from frontend or API layers.

2. **Dependency Injection:**
Use Symfony’s service container; avoid static/global dependencies.

3. **Strict Typing:**
Always use `declare(strict_types=1);` and explicit type declarations.

4. **Doctrine Entities:**
- Keep entities simple (no business logic).
- Mirror schema changes in `resources/Database/Schema.sql`.
- Maintain backward compatibility with phpList 3.

5. **Symfony Best Practices:**
Follow Symfony structure and naming conventions. Use annotations or attributes for routing.

6. **Error Handling & Logging:**
- Prefer structured logging via Graylog.
- Catch and handle exceptions at service or command boundaries.

7. **Async Email:**
- Uses Symfony Messenger.
- Handlers must be idempotent and retry-safe.
- Avoid blocking or synchronous email sending.

---

## 🧪 Testing Guidelines

- **Framework:** PHPUnit
- **Database:** SQLite or mocks for unit tests; MySQL for integration tests.
- **Coverage target:** ≥85% for core logic.
- **Naming:** Mirror source structure (e.g., `Mailer.php` → `MailerTest.php`).


## 🧱 Code Style

- Follow PSR-12 and Symfony coding conventions.
- Match the current codebase’s formatting and spacing.
- Use meaningful, consistent naming.
- Apply a single responsibility per class.


## 🔄 Pull Request Review Guidelines
### 🔐 Security Review Notes

- Do not log sensitive data (passwords, tokens, SMTP credentials).
- Sanitize all user and external inputs.
- Always use parameterized Doctrine queries.
- Async jobs must be retry-safe and idempotent.
10 changes: 2 additions & 8 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
### Summary

Provide a general description of the code changes in your pull request …
were there any bugs you had fixed? If so, mention them. If these bugs have open
GitHub issues, be sure to tag them here as well, to keep the conversation
linked together.

"@coderabbitai summary"

### Unit test

Expand All @@ -17,7 +11,7 @@ You can run the existing unit tests using this command:

### Code style

Have you checked that you code is well-documented and follows the PSR-2 coding
Have you checked that your code is well-documented and follows the PSR-2 coding
style?

You can check for this using this command:
Expand Down
100 changes: 100 additions & 0 deletions src/Domain/Identity/Command/ImportDefaultsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace PhpList\Core\Domain\Identity\Command;

use Doctrine\ORM\EntityManagerInterface;
use PhpList\Core\Domain\Identity\Model\Dto\CreateAdministratorDto;
use PhpList\Core\Domain\Identity\Model\PrivilegeFlag;
use PhpList\Core\Domain\Identity\Repository\AdministratorRepository;
use PhpList\Core\Domain\Identity\Service\AdministratorManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;

#[AsCommand(
name: 'phplist:defaults:import',
description: 'Imports default values into the database (e.g., default admin with all privileges).'
)]
class ImportDefaultsCommand extends Command
{
private const DEFAULT_LOGIN = 'admin';
private const DEFAULT_EMAIL = 'admin@example.com';

public function __construct(
private readonly AdministratorRepository $administratorRepository,
private readonly AdministratorManager $administratorManager,
private readonly EntityManagerInterface $entityManager,
) {
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$login = self::DEFAULT_LOGIN;
$email = self::DEFAULT_EMAIL;
$envPassword = getenv('PHPLIST_ADMIN_PASSWORD');
$envPassword = is_string($envPassword) && trim($envPassword) !== '' ? $envPassword : null;

$allPrivileges = $this->allPrivilegesGranted();

$existing = $this->administratorRepository->findOneBy(['loginName' => $login]);
if ($existing === null) {
// If creating the default admin, require a password. Prefer env var, else prompt for input.
$password = $envPassword;
if ($password === null) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question('Enter password for default admin (login "admin"): ');
$question->setHidden(true);
$question->setHiddenFallback(false);
$password = (string) $helper->ask($input, $output, $question);
if (trim($password) === '') {
$output->writeln('<error>Password must not be empty.</error>');
return Command::FAILURE;
}
}

$dto = new CreateAdministratorDto(
loginName: $login,
password: $password,
email: $email,
isSuperUser: true,
privileges: $allPrivileges,
);
$admin = $this->administratorManager->createAdministrator($dto);
$this->entityManager->flush();

$output->writeln(sprintf(
'Default admin created: login="%s", email="%s", superuser=yes, privileges=all',
$admin->getLoginName(),
$admin->getEmail()
));
} else {
$output->writeln(sprintf(
'Default admin already exists: login="%s", email="%s"',
$existing->getLoginName(),
$existing->getEmail(),
));
}

return Command::SUCCESS;
}

/**
* @return array<string,bool>
* @SuppressWarnings(PHPMD.StaticAccess)
*/
private function allPrivilegesGranted(): array
{
$all = [];
foreach (PrivilegeFlag::cases() as $flag) {
$all[$flag->value] = true;
}
return $all;
}
}
49 changes: 0 additions & 49 deletions src/Migrations/Version20251103SeedInitialAdmin.php

This file was deleted.

Loading