carthage-software/mago
Mago is an extremely fast PHP linter, formatter, and static analyzer written in Rust. It brings Rust-inspired speed and reliability to PHP projects with a modern toolchain and great developer experience, plus multiple install options (script, Homebrew, Composer).
title: BestPractices rules
This document details the rules available in the BestPractices category.
| Rule | Code |
|---|---|
| Combine Consecutive Issets | combine-consecutive-issets |
| Final Controller | final-controller |
| Loop Does Not Iterate | loop-does-not-iterate |
| Middleware In Routes | middleware-in-routes |
| No Array Accumulation In Loop | no-array-accumulation-in-loop |
| No Direct Database Queries | no-direct-db-query |
| No ini_set | no-ini-set |
| No Inline | no-inline |
| No Parameter Shadowing | no-parameter-shadowing |
| No Sprintf Concat | no-sprintf-concat |
| Prefer Anonymous Migration | prefer-anonymous-migration |
| Prefer Arrow Function | prefer-arrow-function |
| Prefer Early Continue | prefer-early-continue |
| Prefer First Class Callable | prefer-first-class-callable |
| Prefer Interface | prefer-interface |
| Prefer Pre-Increment | prefer-pre-increment |
| Prefer Static Closure | prefer-static-closure |
| Prefer Test Attribute | prefer-test-attribute |
| Prefer View Array | prefer-view-array |
| Prefer While Loop | prefer-while-loop |
| Psl Array Functions | psl-array-functions |
| Psl Data Structures | psl-data-structures |
| Psl DateTime | psl-datetime |
| Psl Math Functions | psl-math-functions |
| Psl Output | psl-output |
| Psl Randomness Functions | psl-randomness-functions |
| Psl Regex Functions | psl-regex-functions |
| Psl Sleep Functions | psl-sleep-functions |
| Psl String Functions | psl-string-functions |
| Require Namespace | require-namespace |
| Single Class Per File | single-class-per-file |
| Sorted Integer Keys | sorted-integer-keys |
| Use Compound Assignment | use-compound-assignment |
| Use WordPress API Functions | use-wp-functions |
| Yoda Conditions | yoda-conditions |
combine-consecutive-issetsSuggests combining consecutive calls to isset() when they are joined by a logical AND.
For example, isset($a) && isset($b) can be turned into isset($a, $b), which is more concise
and avoids repeated function calls. If one or both isset() calls are wrapped in parentheses,
the rule will still warn, but it will not attempt an automated fix.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
if (isset($a, $b)) {
// ...
}
<?php
if (isset($a) && isset($b)) {
// ...
}
final-controllerEnforces that controller classes are declared as final.
In modern MVC frameworks, controllers should be treated as entry points that orchestrate the application's response to a request. They are not designed to be extension points.
Extending controllers can lead to deep inheritance chains, making the codebase rigid and difficult to maintain. It's a best practice to favor composition (injecting services for shared logic) over inheritance.
If a controller is intended as a base for others, it should be explicitly marked as abstract. All other concrete controllers should be final to prevent extension.
SymfonyLaravelTempestSpiralCakePHPYii| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"error" |
<?php
namespace App\Http\Controllers;
final class UserController
{
// ...
}
<?php
namespace App\Http\Controllers;
class UserController
{
// ...
}
loop-does-not-iterateDetects loops (for, foreach, while, do-while) that unconditionally break or return before executing even a single iteration. Such loops are misleading or redundant since they give the impression of iteration but never actually do so.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
for ($i = 0; $i < 3; $i++) {
echo $i;
if ($some_condition) {
break; // This break is conditional.
}
}
<?php
for ($i = 0; $i < 3; $i++) {
break; // The loop never truly iterates, as this break is unconditional.
}
middleware-in-routesThis rule warns against applying middlewares in controllers.
Middlewares should be applied in the routes file, not in the controller.
Laravel| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
// routes/web.php
Route::get('/user', 'UserController@index')->middleware('auth');
<?php
namespace App\Http\Controllers;
class UserController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
}
no-array-accumulation-in-loopDetects O(n²) array accumulation patterns inside loops.
Calling array_merge(), array_merge_recursive(), array_unique(), or
array_values() on an accumulator inside a loop copies the entire array on
every iteration. Similarly, using spread syntax ([...$result, ...$item])
in a reassignment has the same cost.
Collect items first and transform once after the loop instead.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
false |
level |
string |
"warning" |
<?php
$chunks = [];
foreach ($items as $item) {
$chunks[] = $item;
}
$result = array_merge(...$chunks);
<?php
$result = [];
foreach ($items as $item) {
$result = array_merge($result, $item);
}
no-direct-db-queryThis rule flags all direct method calls on the global $wpdb object. Direct database queries
bypass the WordPress object cache, which can lead to poor performance. Using high-level functions
like get_posts() is safer and more efficient.
WordPress| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
$posts = get_posts(['author' => $author_id]);
<?php
global $wpdb;
$posts = $wpdb->get_results("SELECT * FROM {$wpdb->posts} WHERE post_author = 1");
no-ini-setEnforces that ini_set is not used.
Runtime configuration changes via ini_set make application behavior unpredictable and environment-dependent. They can mask misconfigured servers, introduce subtle bugs, and lead to inconsistent behavior between development, testing, and production environments.
Modern applications should rely on well-defined configuration through php.ini or framework specific configuration. This ensures that configuration is explicit, consistent, and controlled across all environments.
If a setting truly needs to vary between contexts, it should be handled at the infrastructure or framework configuration level, never by calling ini_set within the application code.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
// In framework config files (e.g., wp-config.php), use constants.
define( 'WP_DEBUG', true );
// Use framework-provided functions where available.
wp_raise_memory_limit( 'admin' );
<?php
// This can override server settings in an unpredictable way.
ini_set( 'display_errors', 1 );
ini_set( 'memory_limit', '256M' );
no-inlineDisallows inline content (text outside of PHP tags) in source files.
Most modern PHP applications are source-code only and do not use PHP as a templating
language. Inline content before <?php, after ?>, or between PHP tags is typically
unintentional and can cause issues such as unexpected output or "headers already sent"
errors.
This rule is disabled by default and is intended for codebases that do not use PHP templates.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
false |
level |
string |
"error" |
<?php
namespace App;
echo "Hello, world!";
Hello
<?php
echo "Hello, world!";
?>
Goodbye
no-parameter-shadowingDetects when a function or method parameter is shadowed by a loop variable or catch variable, making the original parameter value inaccessible.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
false |
level |
string |
"warning" |
<?php
function read(array $domains, array $locales): void
{
$translations = getTranslations($domains, $locales);
foreach ($translations as $namespace => $namespaceLocales) {
// $locales is still accessible
}
}
<?php
function read(array $domains, array $locales): void
{
$translations = getTranslations($domains, $locales);
foreach ($translations as $namespace => $locales) {
// $locales now refers to the loop value, original argument is lost
}
}
no-sprintf-concatDisallows string concatenation with the result of an sprintf call.
Concatenating with sprintf is less efficient and can be less readable than
incorporating the string directly into the format template. This pattern
creates an unnecessary intermediate string and can make the final output
harder to see at a glance.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
$name = 'World';
$greeting = sprintf('Hello, %s!', $name);
<?php
$name = 'World';
$greeting = 'Hello, ' . sprintf('%s!', $name);
prefer-anonymous-migrationPrefer using anonymous classes for Laravel migrations instead of named classes. Anonymous classes are more concise and reduce namespace pollution, making them the recommended approach for migrations.
Laravel| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('flights', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('airline');
$table->timestamps();
});
}
public function down(): void {
Schema::drop('flights');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class MyMigration extends Migration {
public function up(): void {
Schema::create('flights', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('airline');
$table->timestamps();
});
}
public function down(): void {
Schema::drop('flights');
}
}
return new MyMigration();
prefer-arrow-functionPromotes the use of arrow functions (fn() => ...) over traditional closures (function() { ... }).
This rule identifies closures that consist solely of a single return statement and suggests converting them to arrow functions.
7.4.0| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"help" |
<?php
$a = fn($x) => $x + 1;
<?php
$a = function($x) {
return $x + 1;
};
prefer-early-continueSuggests using early continue pattern when a loop body contains only a single if statement.
This improves code readability by reducing nesting and making the control flow more explicit.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"help" |
max_allowed_statements |
integer |
0 |
<?php
for ($i = 0; $i < 10; $i++) {
if (!$condition) {
continue;
}
doSomething();
}
<?php
for ($i = 0; $i < 10; $i++) {
if ($condition) {
doSomething();
}
}
prefer-first-class-callablePromotes the use of first-class callable syntax (...) for creating closures.
This rule identifies closures and arrow functions that do nothing but forward their arguments to another function or method. In such cases, the more concise and modern first-class callable syntax, introduced in PHP 8.1, can be used instead. This improves readability by reducing boilerplate code.
By default, this rule only checks method and static method calls. Optionally, function calls can also
be checked by enabling check-functions, but this may produce false positives with internal PHP
functions that enforce strict argument counts.
8.1.0| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
check-functions |
boolean |
false |
<?php
$names = ['Alice', 'Bob', 'Charlie'];
$uppercased_names = array_map($formatter->format(...), $names);
<?php
$names = ['Alice', 'Bob', 'Charlie'];
$uppercased_names = array_map(fn($name) => $formatter->format($name), $names);
prefer-interfaceDetects when an implementation class is used instead of the interface.
Symfony| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"note" |
<?php
use Symfony\Component\Serializer\SerializerInterface;
class UserController
{
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
}
<?php
use Symfony\Component\Serializer\Serializer;
class UserController
{
public function __construct(Serializer $serializer)
{
$this->serializer = $serializer;
}
}
prefer-pre-incrementEnforces the use of pre-increment (++$i) and pre-decrement (--$i) over
post-increment ($i++) and post-decrement ($i--).
Pre-increment is marginally more efficient and is the convention used by the Symfony coding standards.
Symfony| Option | Type | Default |
|---|---|---|
enabled |
boolean |
false |
level |
string |
"help" |
<?php
++$i;
--$count;
<?php
$i++;
$count--;
prefer-static-closureSuggests adding the static keyword to closures and arrow functions that don't use $this.
Static closures don't bind $this, making them more memory-efficient and their intent clearer.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"help" |
<?php
class Foo {
public function bar() {
// Static closure - doesn't use $this
$fn = static fn($x) => $x * 2;
// Non-static - uses $this
$fn2 = fn() => $this->doSomething();
// Static function - doesn't use $this
$closure = static function($x) {
return $x * 2;
};
}
}
<?php
class Foo {
public function bar() {
// Missing static - doesn't use $this
$fn = fn($x) => $x * 2;
// Missing static - doesn't use $this
$closure = function($x) {
return $x * 2;
};
}
}
prefer-test-attributeSuggests using PHPUnit's #[Test] attribute instead of the test method name prefix.
When a method name starts with test, it can be replaced with a #[Test] attribute
and a shorter method name. This is the modern PHPUnit style (PHPUnit 10+).
PHPUnit| Option | Type | Default |
|---|---|---|
enabled |
boolean |
false |
level |
string |
"warning" |
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
class UserTest extends TestCase
{
#[Test]
public function itReturnsFullName(): void {}
}
<?php
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
public function testItReturnsFullName(): void {}
}
prefer-view-arrayPrefer passing data to views using the array parameter in the view() function,
rather than chaining the with() method.`
Using the array parameter directly is more concise and readable.
Laravel| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"help" |
<?php
return view('user.profile', [
'user' => $user,
'profile' => $profile,
]);
<?php
return view('user.profile')->with([
'user' => $user,
'profile' => $profile,
]);
prefer-while-loopSuggests using a while loop instead of a for loop when the for loop does not have any
initializations or increments. This can make the code more readable and concise.
| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"note" |
<?php
while ($i < 10) {
echo $i;
$i++;
}
<?php
for (; $i < 10;) {
echo $i;
$i++;
}
psl-array-functionsThis rule enforces the usage of Psl array functions over their PHP counterparts. Psl array functions are preferred because they are type-safe and provide more consistent behavior.
Psl| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
$filtered = Psl\Vec\filter($xs, fn($x) => $x > 2);
<?php
$filtered = array_filter($xs, fn($x) => $x > 2);
psl-data-structuresThis rule enforces the usage of Psl data structures over their SPL counterparts.
Psl data structures are preferred because they are type-safe and provide more consistent behavior.
Psl| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
<?php
declare(strict_types=1);
use Psl\DataStructure\Stack;
$stack = new Stack();
<?php
declare(strict_types=1);
$stack = new SplStack();
psl-datetimeThis rule enforces the usage of Psl DateTime classes and functions over their PHP counterparts.
Psl DateTime classes and functions are preferred because they are type-safe and provide more consistent behavior.
Psl| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
string |
"warning" |
How can I help you explore Laravel packages today?