tomasvotruba / ctor
Prefer constructor over always called setters
Requires
- php: ^7.4|^8.0
- phpstan/phpstan: ^2.2
- webmozart/assert: ^1.11|^2.4
README
⚠️ This package is deprecated. The rule has been merged into symplify/phpstan-rules, use it instead:
composer require symplify/phpstan-rules --dev
Then enable the rule in your phpstan.neon:
parameters: ctor: true
If you can use constructor instead of setters, use it. These PHPStan rules will help you to find such cases.
What It Does
This tool collects instances of new object() followed by a series of method calls on the same object:
$human = new Human(); $human->setName('Tomas'); $human->setAge(35);
...and suggests turning them into constructor arguments:
$human = new Human('Tomas', 35); // named arguments work too, if the constructor grows wide $human = new Human(name: 'Tomas', age: 35);
Both set* and add* method prefixes are treated as setters, so $collection->addItem(...) chains are flagged the same way.
Why?
Such chained setters often indicate implicit required dependencies. By moving them to the constructor, you make the object state explicit, safer, and easier to reason about — and even easier to test.
Requirements
- PHP
7.2+ - PHPStan
^2.1
Installation
composer require tomasvotruba/ctor --dev
Usage
Use phpstan/extension-installer to load the extension automatically. Run PHPStan and the rule will pick up.
Without the extension installer, include the config manually in your phpstan.neon:
includes: - vendor/tomasvotruba/ctor/config/extension.neon
What You'll See
When the rule fires, PHPStan reports:
Class "App\Human" is always created with same 2 setter(s): "setAge()", "setName()"
Pass these values via constructor instead
The error identifier is tv.newOverSetters — use it to ignore specific cases via PHPStan's ignoreErrors.
When the Rule Fires (and When It Doesn't)
The rule is intentionally conservative. It only reports a class when:
- The same class is instantiated at least twice across the analysed code, with the same set of setters each time. A single
new + settersblock on its own is not enough — there must be a repeated pattern.
It deliberately skips:
- Doctrine entities (files containing
@ORM\Entityor#[Entity]) - Symfony
HttpKernel\Kernelsubclasses - Vendor code
new + settersblocks interrupted by areturnorthrow(likely conditional construction)
Happy coding!