Search by

wexample / symfony-dev

weeger

Development tools for Symfony

Package info

github.com/wexample/symfony-dev

pkg:composer/wexample/symfony-dev

Statistics

Installs: 1 761

Dependents: 2

Suggesters: 0

Stars: 0

Open Issues: 0

5.0.0 2026-10-08 09:42 UTC

README

Version: 5.0.0

wexample/symfony-dev is a Symfony bundle of development tools, registered in the dev and test environments only. It ships three things.

The demonstration data. An application declares one class implementing SeederInterface — what its rows are — and gets the rest: dev:seed empties the database and fills it again, reproducibly (the same --seed gives the same rows, so acceptance scenarios can name what they walk through), POST /_dev/seed does the same from the browser, and wexample/symfony-dev-ds adds « reload the demonstration data » to the development menu of symfony-design-system.

Different data for the tests. A demonstration and a test suite rarely want the same rows. Give each its own seeder and restrict it with Symfony's #[When]: a class marked #[When(env: 'dev')] loads the demonstration, one marked #[When(env: 'test')] loads what the tests rely on, and SeedService only ever sees the seeders of the environment it runs in. A test fills its database with the same call the command makes — static::getContainer()->get(SeedService::class)->seed() — which empties the database first, so it is a whole reset rather than an addition.

The development environment. dev:setup installs the suite's local PHP and JS packages as vendor/ symlinks without dirtying composer.lock, and dev:change-user-password / dev:change-all-users-password rewrite password hashes.

The coding conventions. A collection of Rector rules enforcing them automatically: controllers must be final and carry a global #[Route] name prefix, route names on methods must match their PHP method names, form and entity classes must carry the right class suffixes, entity #[Column] types must reference Types::* constants, and role-based test files must exist for every controller. They are referenced from the consuming project's rector.php, so the conventions are applied by a code-mod tool rather than enforced by hand in code review.

Table of Contents

Architecture

The bundle is built from four independent layers: a DependencyInjection layer that reads configuration and wires services, a Command layer that automates the development environment, a demonstration data layer that empties the database and fills it again, and a Rector layer that enforces coding conventions by transforming PHP source files. Each layer is self-contained and does not call into the others.

Entry point

src/WexampleSymfonyDevBundle.php is an empty subclass of Symfony's Bundle. Symfony discovers it, which triggers src/DependencyInjection/WexampleSymfonyDevExtension.php.

DependencyInjection

src/DependencyInjection/WexampleSymfonyDevExtension.php calls loadConfig() (from the parent AbstractWexampleSymfonyExtension) to import src/Resources/config/services.yaml, then processes the user-supplied configuration and stores the result as container parameters:

  • wexample_symfony_dev.vendor_dev_paths — glob patterns pointing to local PHP package directories.
  • wexample_symfony_dev.js_dev_packages — glob patterns pointing to local JS package directories.
  • wexample_symfony_dev.setup_hooks — extra Symfony commands to run during dev:setup.

src/DependencyInjection/Configuration.php defines the tree. All three keys are optional arrays of strings (or, for setup_hooks, arrays of {command, args} maps).

src/Resources/config/services.yaml autowires every class under Command/, Controller/ and Service/ so no manual service definition is required.

The extension also registers SeederInterface for autoconfiguration, so an application's seeder is found by its interface alone, with nothing to declare.

Command layer

The setup and password commands extend src/Command/AbstractDevCommand.php, which:

  • reads wexample_symfony_dev.vendor_dev_paths from the parameter bag into $this->devVendors;
  • provides forEachDevPackage(callable) to iterate every composer.json-bearing directory found under those paths;
  • provides reloadAutoloader(OutputInterface), which runs composer dump-autoload --classmap-authoritative;
  • provides helpers to read and write the application's composer.json.

dev:setup

src/Command/SetupCommand.php is the top-level orchestrator. It runs, in order:

  1. dev:setup-node --force
  2. dev:setup-composer
  3. Any hooks from wexample_symfony_dev.setup_hooks
  4. reloadAutoloader()
  5. php bin/console cache:clear

dev:setup-composer

src/Command/SetupComposerCommand.php installs local PHP packages as vendor/ symlinks without dirtying composer.lock. The sequence is:

  1. Back up composer.json (in memory) and composer.lock (to composer.lock.backup).
  2. Inject path repositories derived from wexample_symfony_dev.vendor_dev_paths into a temporary composer.json, with canonical: false and symlink: true.
  3. Relax any exact version constraints on those packages to ^x.y.z.
  4. Remove the existing installed copies from vendor/.
  5. Remove composer.lock and run composer install.
  6. Restore the original composer.json and composer.lock.
  7. Force-symlink every local source directory over whatever Composer resolved, so the working copy always wins regardless of Packagist version resolution.

dev:setup-node

src/Command/SetupNodeCommand.php handles the JS side. It:

  1. Creates a symlink <package>/node_modules → <project>/node_modules for every local package that ships an assets/package.json.
  2. Runs yarn install inside each directory listed by JsDevPackagesResolver, skipping packages whose node_modules already exists unless --force is passed.

dev:change-user-password and dev:change-all-users-password

src/Command/ChangeUserPasswordCommand.php finds a single user by an identifier field (default: email) and replaces their password hash. src/Command/ChangeAllUsersPasswordCommand.php does the same for every record in the repository. Both default to App\Entity\User and accept --class to point at a different entity.

dev:seed

src/Command/SeedCommand.php extends AbstractBundleCommand directly — it needs neither the kernel nor the dev vendor paths. It refuses to run when the application declares no seeder, then refuses again without --force, since the first thing it does is empty the database. --seed (default 2026) is the number the dataset is drawn from. What was written is listed back, by name.

Demonstration data

The content belongs to the application, the mechanism to this bundle. An application declares one class implementing src/Interface/SeederInterface.php — load(), returning what it wrote as ['patients' => 53] — and nothing else: the interface is registered for autoconfiguration, so the service is found by its type.

src/Service/SeedService.php is the mechanism. seed(int $seed) seeds mt_rand(), empties the database, runs every tagged seeder, flushes once, and returns the merged report. Seeding the random source before the seeders run is what makes the dataset reproducible: the same seed gives the same rows, so an acceptance scenario can name the rows it walks through. hasSeeders() answers whether the application declared anything, which the command and the menu entry both ask before offering to reload nothing.

src/Service/DatabaseResetService.php empties every table, doctrine_migration_versions left alone — truncating it would have the application believe no migration ever ran. It takes the DBAL Connection and nothing more, so a test can hand it one directly. PostgreSQL gets a single TRUNCATE … CASCADE; MySQL and MariaDB get one TRUNCATE TABLE per table with FOREIGN_KEY_CHECKS lifted around them, since the order the tables come in is nobody's choice; anything else, SQLite among them, gets a DELETE FROM per table.

src/Controller/SeedController.php is the route the development menu posts to: POST /_dev/seed, named dev_seed, declared env: ['dev', 'test']. It is reached by a program and read by nobody, hence the /_dev path. The sign-in is asked for with isGranted() and not with #[IsGranted]: without the security bundle the attribute is read by nobody, where isGranted() throws — a route emptying a database fails closed. It answers 404 when the application declares no seeder, so the database is never emptied for nothing. The CSRF token is checked next, then the visitor is sent to /, signed out with the accounts that were replaced. An application loading its routes with routing.controllers gets it with every controller service; another imports src/Resources/config/routes.yaml.

The entry of the development menu posting to that route is not here: it is front-end, and lives in symfony-dev-ds, which depends on symfony-design-system. This package depends on no front-end package — symfony-loader depends on it, so the other way round would be a cycle.

Service

src/Service/JsDevPackagesResolver.php is consumed by SetupNodeCommand. It reads wexample_symfony_dev.js_dev_packages, expands any glob patterns, opens the package.json of each matching directory, and returns a map of name → absolute path. It is the single place that translates glob configuration into concrete filesystem paths for the Node tooling.

Helper

src/Helper/DevHelper.php holds the single constant VENDOR_DIR_NAME = 'vendor'.

Rector layer

The Rector layer is a collection of rules that Rector applies to PHP source files. Rules are not loaded by Symfony and carry no service registrations; they are referenced directly in the consuming project's rector.php.

Abstract base classes

src/Rector/AbstractRector.php extends Rector's own AbstractRector and adds shared utilities used by every rule in this bundle:

  • getReflexion(Node) — retrieves the ClassReflection for any node from PHPStan's scope.
  • isSubclassOf(Node, string) and nodeIsSubclassOf(Node, string) — inheritance checks via reflection.
  • isTraitUsed(Node, string) — resolves parsed traits through AstResolver.
  • renderTemplate(string, array) — simple `` template rendering used to generate test file stubs.
  • buildConstantArgument(string, string) — constructs a ClassConstFetch AST node for a class constant reference.

src/Rector/AbstractControllerMethodNameRector.php targets ClassMethod nodes. refactor() checks whether the method belongs to a class that extends AbstractController; if so, it hands the method to the abstract refactorMethod(ClassMethod, ReflectionMethod) that each subclass implements. It also provides buildNodeControllerRoutePrefix(Node), which derives the expected route prefix from the controller's class name.

src/Rector/AbstractClassSuffixRector.php targets Class_ nodes. refactor() checks whether the class lives under getClassBasePath() and lacks the suffix returned by getClassSuffix(); if so, it renames the class with ClassRenamer, updates the rename map in RenamedClassesDataCollector, and schedules the file to be moved via RemovedAndAddedFilesCollector.

src/Rector/AbstractEntityManipulatorRector.php targets Class_ nodes that are concrete subclasses of a manipulator class (a crud service or controller). For each such class, it calls getEntityClassName() on the class itself, derives the expected cousin trait path through EntitySyntaxService, and inserts the trait as the first trait if it is not already present.

Concrete rules

File What it changes
src/Rector/ControllerClassIsFinalRector.php Adds final to any non-abstract controller class.
src/Rector/ControllerHasGlobalRouteNameRector.php Adds #[Route(name: 'controller_prefix_')] to a final controller class if the attribute or its name argument is missing.
src/Rector/ControllerMethodHasRouteRector.php Adds #[Route(name: 'snake_method_name')] to any public, non-static, non-magic controller method that lacks one.
src/Rector/ControllerRouteNameHasNoControllerPrefixRector.php Strips the controller prefix from a method-level #[Route(name:)] that duplicates the class-level prefix.
src/Rector/ControllerMethodNameMatchRouteNameRector.php Renames the PHP method to the camelCase form of its #[Route(name:)] value.
src/Rector/ControllerClassHasConstantsAsRoutesNamesRector.php Replaces string route name literals in #[Route(name:)] with self::ROUTE_* constants, creating the constant if absent.
src/Rector/FormSuffixRector.php Ensures Form classes under ClassHelper::CLASS_FORM_BASE_PATH end with the Form suffix.
src/Rector/FormProcessorSuffixRector.php Ensures FormProcessor classes end with the FormProcessor suffix.
src/Rector/EntityHasConstantsAsPropertiesTypes.php Replaces #[Column(type: 'string')] string literals with Types::STRING-style Doctrine constants.
src/Rector/EntityManipulatorHasTraitCrudServiceRector.php Ensures concrete crud services use their entity's manipulator trait.
src/Rector/EntityManipulatorHasTraitControllerRector.php Ensures concrete entity controllers use their entity's manipulator trait, skipping abstract and virtual controllers.
src/Rector/TestControllerHasRolesTestsRector.php Creates missing role-based test class files (e.g. AnonymousMyControllerTest.php) for each final controller, rendering them from a project-side Twig stub. Skipped when #[RectorIgnoreControllerRoleTest] is present on the controller.
src/Rector/TestControllerHasMethodsRector.php Adds a failing test* stub method to a controller test class for each route method in the corresponding controller that has no test yet.
src/Rector/TestControllerHasNoOrphanMethodsRector.php Removes test* methods from a controller test class whose corresponding controller route method no longer exists.
src/Rector/TestControllerInheritsFromParentRoleRector.php Updates a role-based test class to extend the correct parent test class derived from the role hierarchy.

Rector traits

Traits are mixed into the rules above to share logic without repeating it.

src/Rector/Traits/ControllerRectorTrait.php identifies whether a class node is a controller (isInstanceOfAbstractControllerClass), a final controller (isFinalControllerClass), or a controller test class (isControllerTestClass). It also provides forEachTestableOriginalControllerMethod, which reflects the real controller referenced by a test class and invokes a callback for each route method.

src/Rector/Traits/MethodRectorTrait.php provides three helpers: isPublicAndNotMagic(ReflectionMethod), getNodeMethod(Node) (which wraps the reflection lookup in a try/catch for mid-refactor renames), and changeMethodName(ClassMethod, string).

src/Rector/Traits/AttributeRectorTrait.php is a CRUD layer for PHP 8 attribute nodes: find an attribute by class name, check whether a named argument already exists, add an attribute group, add or mutate an argument value (string literal or class constant fetch).

src/Rector/Traits/ConstantRectorTrait.php provides hasClassConstant(Class_, string), which walks the class's constant list to check for a given name.

src/Rector/Traits/RoleRectorTrait.php parses config/packages/security.yaml at analysis time and exposes getParentRole(string) for navigating the role hierarchy.

Rector attribute

src/Rector/Attribute/RectorIgnoreControllerRoleTest.php is a plain PHP attribute (#[Attribute]). Attaching it to a controller class tells TestControllerHasRolesTestsRector to skip that class.

Integration in the Suite

This package is part of the Wexample Suite — a collection of high-quality, modular tools designed to work seamlessly together across multiple languages and environments.

Related Packages

The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.

Visit the Wexample Suite documentation for the complete package ecosystem.

Dependencies

  • php: >=8.5
  • wexample/symfony-helpers: >=15.0.0
  • wexample/symfony-testing: >=6.0.0

Versioning & Compatibility Policy

Wexample packages follow Semantic Versioning (SemVer):

  • MAJOR: Breaking changes
  • MINOR: New features, backward compatible
  • PATCH: Bug fixes, backward compatible

We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Free to use in both personal and commercial projects.

About us

Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.

This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.

Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.

Migration Notes

When upgrading between major versions, refer to the migration guides in the documentation.

Breaking changes are clearly documented with upgrade paths and examples.