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
6 changes: 3 additions & 3 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ jobs:

- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
php-version: '8.5'
coverage: none

- name: Cache Composer downloads
uses: actions/cache@v4
with:
path: ~/.cache/composer/files
key: composer-${{ runner.os }}-php-8.4-${{ hashFiles('composer.json') }}
restore-keys: composer-${{ runner.os }}-php-8.4-
key: composer-${{ runner.os }}-php-8.5-${{ hashFiles('composer.json') }}
restore-keys: composer-${{ runner.os }}-php-8.5-

- name: Install dependencies
run: composer update --no-interaction --prefer-dist --prefer-stable
Expand Down
141 changes: 52 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
[![Packagist downloads](https://img.shields.io/packagist/dt/cakephp/mago-rules?logo=packagist&label=downloads)](https://packagist.org/packages/cakephp/mago-rules)
[![Mago compatibility](https://img.shields.io/badge/Mago-%5E1.47-f97316)](https://mago.carthage.software/)

A Mago-first coding-standard preset for CakePHP applications and plugins. Mago
owns generic formatting, linting, code-quality checks, and fixes; this package
adds CakePHP formatter preferences and the few framework conventions that Mago
cannot express natively.
A coding-standard preset for CakePHP applications and plugins, combining
[Mago](https://mago.carthage.software/)'s formatter and linter with CakePHP conventions.

Requires PHP `^8.4` and Mago `^1.47`. Designed for CakePHP 6.x applications and plugins.

## Installation

Expand All @@ -22,110 +22,73 @@ Import the preset from the project's `mago.toml`:
extends = "vendor/cakephp/mago-rules/cakephp.mago.toml"
```

The imported configuration starts the package-owned extension worker. No
copied bootstrap or manual rule registration is needed.

## Usage

Use Mago's normal commands:
Check formatting and lint your project:

```sh
mago format --check
mago lint
vendor/bin/mago format --check
vendor/bin/mago lint
```

Apply formatting and safe lint fixes with:

```sh
mago format
mago lint --fix --format-after-fix --fail-on-remaining
vendor/bin/mago format
vendor/bin/mago lint --fix --format-after-fix --fail-on-remaining
```

Adopting the preset is intentionally a code-quality migration. Existing
CakePHP repositories are expected to have formatter changes and native Mago
findings on their first run. Review the formatting as a dedicated change, then
either address lint findings incrementally or establish a baseline:
PHPUnit assertion-style fixes require the additional `--potentially-unsafe` flag.
Review those changes before keeping them.

```sh
mago format --dry-run
mago lint --stats
mago lint --baseline mago-baseline.toml --generate-baseline
mago lint --baseline mago-baseline.toml
mago lint --baseline mago-baseline.toml --remove-outdated-baseline-entries
```
## What the preset includes

The preset starts from Mago's default rules and applies CakePHP formatting and
style choices, including short arrays, imported class references, camelCase
function names, and `$this->assert…` in PHPUnit tests. It also checks for negated
ternary conditions and parameter shadowing. See [cakephp.mago.toml](cakephp.mago.toml)
for the complete configuration.

CakePHP-specific rules cover:

| Rule | Convention |
| --- | --- |
| `mago-cakephp/trait-suffix` | Trait names end in `Trait` |
| `mago-cakephp/public-method-underscore` | No underscore-prefixed methods, except magic methods and Entity accessors/mutators |
| `mago-cakephp/elseif` | Use `elseif` rather than `else if` |
| `mago-cakephp/function-docblock` | Document functions and methods outside tests |
| `mago-cakephp/docblock-tag-spacing` | One space after PHPDoc tags |
| `mago-cakephp/throws-tag` | Include an exception type in `@throws` |
| `mago-cakephp/inherit-doc` | Use CakePHP's `@inheritDoc` spelling and placement |

Commit the baseline and remove outdated entries as findings are fixed. New
findings remain visible, so the baseline is a migration tool rather than a
weaker permanent ruleset.
Method-count checks exclude conventional `src/Controller`, `src/Model/Table`, and
`src/View/Helper` paths in applications and plugins. Property-count checks exclude
`src/Model/Entity`. Both checks remain active elsewhere; custom layouts may need
additional exclusions.

The preset starts from Mago's default rule catalogue, then disables language
preferences that conflict with established CakePHP code. Projects can override
native or CakePHP rules after the `extends` declaration:
## Configuration

Override individual rules in your project's `mago.toml` after the `extends` declaration:

```toml
[linter.rules]
cyclomatic-complexity = { enabled = false }
no-parameter-shadowing = { level = "error" }
"mago-cakephp/function-docblock" = { enabled = false }
```

Class references must use imports rather than fully qualified names in executable
code. The native `no-fully-qualified-global-class-like` rule is enabled at error
severity; `mago lint --fix` adds imports and shortens references. PHPDoc names
are unaffected.
For an intentional local exception, use `@mago-expect lint:<rule-code>`.
PHPCS suppression comments are not recognized.

## Responsibility map
## Adopting in an existing project

| Concern | Owner |
| --- | --- |
| PSR-12 layout, braces, imports, quotes, commas, casts and return spacing | Mago formatter with CakePHP settings |
| Imported class references | Native `no-fully-qualified-global-class-like` rule |
| Short arrays, braced blocks, short tags, silenced errors, assignments in conditions and redundant syntax | Native Mago rules |
| Conflicting `empty()`/`isset()`/comparison/named-argument preferences | Disabled by the CakePHP preset |
| Trait `Trait` suffix | `mago-cakephp/trait-suffix` |
| Method underscore prefixes, with Entity accessor/mutator exceptions | `mago-cakephp/public-method-underscore` |
| `elseif` spelling | `mago-cakephp/elseif` |
| Function and method docblocks outside tests | `mago-cakephp/function-docblock` |
| One space after PHPDoc tags | `mago-cakephp/docblock-tag-spacing` |
| Exception type on `@throws` | `mago-cakephp/throws-tag` |
| CakePHP `@inheritDoc` spelling and placement | `mago-cakephp/inherit-doc` |

Use `@mago-expect lint:<rule-code>` for an intentional local exception, or a
Mago baseline when migrating an existing project. PHPCS suppression comments
are not interpreted by this package.

## Relationship to CakePHP CodeSniffer

This is not a PHPCS emulator or a promise of sniff-for-sniff parity. Generic
PHPCS and Slevomat concerns belong to Mago's formatter and native linter, even
when diagnostics or fixes differ. Mago may report additional quality or
security findings because its defaults remain enabled, and formatter output is
not expected to be byte-for-byte identical to PHPCBF.

Rules are added here only when they encode CakePHP-specific policy that native
Mago cannot express. A difference from CakePHP CodeSniffer alone is not a reason
to recreate a sniff. Conversely, a clean CakePHP CodeSniffer run does not imply
a clean Mago run: Mago's maintainability, correctness, and security diagnostics
are part of the value of adopting this preset.

Mago internally understands PHPDoc, but version 1.47's PHP extension SDK exposes
docblocks as source trivia rather than a parsed PHPDoc tree. The extension
therefore implements only the small line-oriented PHPDoc policies listed above.

## Compatibility and development

| Package requirement | Supported version |
| --- | --- |
| PHP | `^8.4` |
| Mago | `^1.47` |
| Primary target | CakePHP 6.x applications and plugins |

Run `composer run cs-check`, `composer test`, `composer run lint-corpus`,
`composer run test-fixes`, `composer run test-formatter`, and
`composer run test-consumer`. To check the CakePHP-specific extension rules for
false positives against CakePHP itself, pass an existing shallow checkout with
`scripts/check-cakephp-6.sh --workspace /path/to/cakephp`; without an argument
the script creates a temporary shallow clone of CakePHP 6.x. Add
`--migration-audit` to run the complete native Mago preset and see the
refactoring backlog. That audit is expected to exit unsuccessfully until the
target codebase has completed its migration. A bare positional path remains
supported as shorthand.
Preview formatting changes with `vendor/bin/mago format --dry-run`. Mago may
report additional findings compared with CakePHP CodeSniffer. To fix existing
lint findings gradually, generate a baseline:

```sh
vendor/bin/mago lint --baseline mago-baseline.toml --generate-baseline
vendor/bin/mago lint --baseline mago-baseline.toml
```

Commit the baseline and use it in subsequent lint runs. Remove resolved entries
with `vendor/bin/mago lint --baseline mago-baseline.toml --remove-outdated-baseline-entries`.
16 changes: 16 additions & 0 deletions cakephp.mago.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ integrations = ["phpunit"]
# preferences that conflict with established CakePHP practices.
# Consuming projects remain free to override any rule.
[linter.rules]
assertion-style = { enabled = true, level = "error", style = "this" }

# Shared naming and readability choices retain Mago's native severity levels.
function-name = { enabled = true, camel = true }
no-negated-ternary = { enabled = true }
no-parameter-shadowing = { enabled = true }

# Runtime INI settings and message-free native assertions are CakePHP idioms.
no-ini-set = { enabled = false }
assert-description = { enabled = false }

# Framework-facing classes expose actions, finders, helpers, and persisted fields.
# Keep native size checks active outside these conventional CakePHP paths.
too-many-methods = { exclude = ["**/src/Controller/**", "**/src/Model/Table/**", "**/src/View/Helper/**"] }
Comment thread
josbeir marked this conversation as resolved.
too-many-properties = { exclude = ["**/src/Model/Entity/**"] }

array-style = { enabled = true, level = "error", style = "short" }
block-statement = { enabled = true, level = "error" }
no-short-opening-tag = { enabled = true, level = "error" }
Expand Down
77 changes: 77 additions & 0 deletions scripts/check-consumer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mkdir -p "$consumer_dir/vendor/cakephp"
ln -s "$repo_root" "$consumer_dir/vendor/cakephp/mago-rules"

mago="$repo_root/vendor/bin/mago"
"$mago" --workspace "$repo_root" format --check tests/policy
"$mago" --workspace "$consumer_dir" extension validate
"$mago" --workspace "$consumer_dir" format --check
"$mago" --workspace "$consumer_dir" lint
Expand All @@ -33,3 +34,79 @@ fi
"$mago" --workspace "$consumer_dir" lint --fix --format-after-fix --fail-on-remaining
diff -u "$repo_root/tests/fixer/class-imports.expected.php" "$consumer_dir/src/ClassImports.php"
"$mago" --workspace "$consumer_dir" lint

# Check that the installed preset rejects static assertions at error severity.
cp "$repo_root/tests/policy/assertion-style.php" "$consumer_dir/tests/AssertionStyleTest.php"
if "$mago" --workspace "$consumer_dir" lint --reporting-format short > "$consumer_dir/assertion-style.log" 2>&1; then
echo "Expected the installed preset to reject static assertions." >&2
exit 1
fi
if ! grep -q 'error\[assertion-style\]' "$consumer_dir/assertion-style.log"; then
cat "$consumer_dir/assertion-style.log" >&2
exit 1
fi
rm "$consumer_dir/tests/AssertionStyleTest.php"

# Exercise native convention choices through the installed preset. Include help
# diagnostics in the failure threshold so default advisory levels are checked.
policy_codes=(
--retain-code function-name
--retain-code no-negated-ternary
--retain-code no-parameter-shadowing
--retain-code no-ini-set
--retain-code assert-description
)
cp "$repo_root/tests/policy/native-conventions.valid.php" "$consumer_dir/src/NativeConventions.php"
if ! "$mago" --workspace "$consumer_dir" lint --minimum-fail-level help \
"${policy_codes[@]}" src/NativeConventions.php > "$consumer_dir/native-conventions.log" 2>&1; then
cat "$consumer_dir/native-conventions.log" >&2
exit 1
fi
cp "$repo_root/tests/policy/native-conventions.invalid.php" "$consumer_dir/src/NativeConventions.php"
if "$mago" --workspace "$consumer_dir" lint --minimum-fail-level help --reporting-format short \
"${policy_codes[@]}" src/NativeConventions.php > "$consumer_dir/native-conventions.log" 2>&1; then
echo "Expected the installed preset to report native convention violations." >&2
exit 1
fi
for diagnostic in 'help[function-name]' 'help[no-negated-ternary]' 'warning[no-parameter-shadowing]'; do
if ! grep -Fq "$diagnostic" "$consumer_dir/native-conventions.log"; then
cat "$consumer_dir/native-conventions.log" >&2
exit 1
fi
done
rm "$consumer_dir/src/NativeConventions.php"

# Native count rules remain active for services, while app and plugin framework
# paths are exempt. Do not use --only: it bypasses the preset's configuration.
check_count_policy() {
local rule="$1"
local fixture="$2"
local prefix location target
shift 2
for prefix in "" "plugins/Example/"; do
for location in "$@"; do
target="$prefix$location/Count.php"
mkdir -p "$consumer_dir/$(dirname "$target")"
cp "$repo_root/tests/policy/$fixture" "$consumer_dir/$target"
if ! "$mago" --workspace "$consumer_dir" lint --retain-code "$rule" "$target" > "$consumer_dir/count.log" 2>&1; then
cat "$consumer_dir/count.log" >&2
exit 1
fi
rm "$consumer_dir/$target"
done
target="${prefix}src/Service/Count.php"
mkdir -p "$consumer_dir/$(dirname "$target")"
cp "$repo_root/tests/policy/$fixture" "$consumer_dir/$target"
if "$mago" --workspace "$consumer_dir" lint --retain-code "$rule" --reporting-format short "$target" > "$consumer_dir/count.log" 2>&1; then
echo "Expected $rule to remain active for $target." >&2
exit 1
fi
if ! grep -Fq "error[$rule]" "$consumer_dir/count.log"; then
cat "$consumer_dir/count.log" >&2
exit 1
fi
rm "$consumer_dir/$target"
done
}
check_count_policy too-many-methods method-count.php src/Controller src/Model/Table src/View/Helper
check_count_policy too-many-properties property-count.php src/Model/Entity
4 changes: 2 additions & 2 deletions tests/Unit/CakePhpExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public function testRegistersTheCakePhpCompatibilityRules(): void
{
$extension = CakePhpExtension::create();

self::assertSame('mago-cakephp/rules', $extension->identifier);
self::assertSame(
$this->assertSame('mago-cakephp/rules', $extension->identifier);
$this->assertSame(
[
'mago-cakephp/trait-suffix',
'mago-cakephp/public-method-underscore',
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/PathMatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ public static function paths(): iterable
#[DataProvider('paths')]
public function testMatchesTestPaths(string $path, bool $expected): void
{
self::assertSame($expected, PathMatcher::matches($path, ['tests/**', '**/tests/**']));
$this->assertSame($expected, PathMatcher::matches($path, ['tests/**', '**/tests/**']));
}
}
4 changes: 2 additions & 2 deletions tests/Unit/RuleIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ public function testRuleFixtures(string $rule, array $fixtures): void
$root,
);

self::assertIsResource($process);
$this->assertIsResource($process);
$output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);

self::assertSame(0, proc_close($process), $output);
$this->assertSame(0, proc_close($process), $output);
}
}
2 changes: 1 addition & 1 deletion tests/cakephp6/mago.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version = "1.47"
php-version = "8.4"
php-version = "8.5"

[source]
paths = ["config", "src"]
Expand Down
16 changes: 16 additions & 0 deletions tests/policy/assertion-style.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);

namespace App\Test;

use PHPUnit\Framework\TestCase;

final class AssertionStyleTest extends TestCase
{
public function testAssertionStyles(): void
{
self::assertSame(1, 1);
static::assertTrue(true);
$this->assertFalse(false);
}
}
Loading
Loading