SuperAPI AI Directory

cakephp-migration

General & other

View repository

Updated 10-15 17:24

About this skill

CakePHP Version Migration Guide (baserCMS)

A collection of breaking changes, deprecations, and fix recipes encountered during CakePHP upgrades.By versionOrganize and grow it. For PHP core causes (implicit nullable, dynamic properties, etc.), see the separate skill php-migration .

Covered: 5.0 → 5.1, 5.1 → 5.2. Version attribution is based on actual observations during migration work (there may be discrepancies with the exact introduction version). When supporting a new version, append a new "## X.Y → X.Z changes" section.


General Principles (Common to All Versions)

  • Upgrade is composer require .composer update is not used(to avoid affecting all dependencies). Example: composer require cakephp/cakephp:"5.2.*" --update-with-dependencies
  • Distinguish between "fatal errors" and "deprecation warnings". In debug mode, deprecation warnings are displayed at the bottom of the screen and look like "errors", butlogs/debug.log の debug: is a warning (execution continues).logs/error.log and Fatal/Exception are the real errors.
  • When a large number of tests fail, classify them by root cause. Even with hundreds of failures, there are only a dozen or so causes. Aggregate exceptions from the full log and eliminate systemic causes first (for aggregation commands and isolation procedures, see the basercms-unittest skill).
  • Distinguish between regressions and existing/environment failures。git diff HEAD -- <file> to check whether it is something your change targets. If it is an unchanged file and depends on fixture/plugin loading (Plugin::isLoaded('X') is false / DI container not registered / English↔Japanese message mismatch), it is not migration-caused but an environmental factor.Because vendor has already been replaced with the new version, a simple git stash cannot create a clean baseline。

For how to run unit tests and procedures for aggregating and isolating failures, see the basercms-unittest skill.


5.0 → 5.1 changes

In 5.1, E_USER_DEPRECATED-type deprecations increase.Error.errorLevel can suppress them, but fix them as part of proper support.

1-1. Query::order() / group() deprecation

order()→orderBy()、group()→groupBy()(SelectQuery::group() も)。

// Before
$query->order(['Contents.id'])->group('site_id');
// After
$query->orderBy(['Contents.id'])->groupBy('site_id');

1-2. find('all', $options)(options array) deprecation (Since 5.0.0)

Passing an options array is deprecated. Use named arguments or spread.

// Before
$this->Table->find('all', ['limit' => $n, 'conditions' => $c]);
// After(いずれか)
$query = $this->Table->find()->where($c); if ($n) $query->limit($n);
$this->Table->find('all', ...$options);   // $options のキーが finder の名前付き引数に一致する場合
$this->Table->find('all', conditions: $c, order: $o);
  • Those that are already ...$options / conditions: etc. are not targeted (beware of false positives).
  • Warning: Calling 'findAll' finder with options array is deprecated. Use named arguments instead.

1-3. Pagination results PaginatedResultSet に

paginate() The return value of PaginatedInterface(PaginatedResultSet) and becomesisEmpty() does not directly have ResultSet methods such asdoes not directly have. First, call ->items() .

// Before(テンプレート等)
if (!$sites->isEmpty()) { ... }
// After
if (!$sites->items()->isEmpty()) { ... }

1-4. Renaming of translation cache settings

_cake_core_ → _cake_translations_。config/app.php・config/bootstrap.php・BcUtil Update references such as

  • config/plugins.php で 'BaserCore' => null Values such as 'BaserCore' => [] set to .

5.1 → 5.2 changes

5.2 has many changes that "turn operations previously silently overwritten/allowed into exceptions."

2-1. Returning values from event listeners is deprecated (becomes an error in 5.3)

From the listener, use return without,$event->setResult() / $event->stopPropagation() for the value. The return type is : void can be set to .

// Before
public function beforeSave(EventInterface $event) { return false; }
// After
public function beforeSave(EventInterface $event): void { $event->setResult(false); }
  • The return values of Table/Marshaller callbacks were originally ignored, so simply return just delete it (seebeforeMarshal は ArrayObject $data ; rewrite by reference. The return value is not used).
  • Controller beforeFilter standard fix:
    // Before
    $response = parent::beforeFilter($event);
    if ($response) return $response;
    ...
    return $this->redirect(...);
    // After
    parent::beforeFilter($event);
    if ($event->getResult()) return;
    ...
    $event->setResult($this->redirect(...));
    return;
    
  • Within the closure, return(SEO etc.) is out of scope (beware of false positives).

2-2. AssociationCollection::add() is a duplicate alias and throws an exception

Until 5.1 it was silently overwritten → 5.2 occurs when an association already defined with Association alias 'X' is already set. を throw。 initialize() , is hasMany/belongsTo etc. in a different method/Service/Controller,reconfigured(e.g. replacing the finder).

// 再設定の前に既存を削除してガードする
if ($table->associations()->has('CustomLinks')) {
    $table->associations()->remove('CustomLinks');
}
$table->hasMany('CustomLinks')->setFinder('all')->...;
  • inline hasMany('X') is safe to consolidate into a guarded dedicated method (e.g. setHasManyLinksByAll()).
  • Detection: grep -rnE "\->(hasMany|belongsTo|belongsToMany|hasOne)\(" --include="*.php" で Scan for association configuration inside Service/Controller(anything other than Table's initialize requires caution).

2-3. Duplicate validation rule names throw an exception

Calling a rule with the same name on the same field twice with add doing so A validation rule with the name 'X' already exists. In 5.1 it was overwritten.

  • If blocks with the same name are duplicated,merge(preserving last-wins intent). Example: consolidate multiple validation definitions for alias ,minLength add it only once, etc.
  • Watch out for collisions with the callback's named argument $context (the second argument of a custom method $context do not set to ).
  • Note: If a test calls "the same method twice on the same validator to verify overwriting", that isa test-side concernand does not happen in the production flow (once per field) = it may be out of scope for app fixes.

2-4. ResultSet type / not cloneable

  • Query::all() The return value of Cake\ORM\ResultSet(previously ResultSetDecorator). Loosen the type declaration to ResultSetInterface か iterable .
  • ResultSet is not cloneable and its iterator is shared。clone $resultSet , or double iterating over a live ResultSet causes elements to disappear.->toArray() でmaterialize early before using it (this causes issues such as form fields disappearing).
  • Absorb helper/service argument types with ResultSetInterface/iterable set to ,is_array($x) ? $x : $x->toArray() .

2-5. Array condition null values throw exceptions

where(['field' => null]) は Expression 'field' has invalid 'null' value. ... operator (IS, IS NOT) is missing. throw. Use the null-safe IS operator.

// Before
$conditions = ['Contents.entity_id' => $entityId];   // $entityId が null だと例外
// After
$conditions = ['Contents.entity_id IS' => $entityId]; // 非nullなら =、nullなら IS NULL

2-6. Entity string casting is deprecated

In templates etc., (string)$entity equivalent (such as direct insertion into HTML attributes) is deprecated.json_encode($entity) use.

$this->BcBaser->js('...', false, ['data-current' => json_encode($content)]);

2-7. PluginCollection::add() throws an exception if already loaded

Plugin named 'X' is already loaded。addPlugin guard before.

if (!$application->getPlugins()->has($name)) {
    $application->addPlugin($name);
}
  • Theme (coreFrontTheme etc.) and $themes beware of cases where same-named entries are added twice in an array (addTheme())。
  • Similarly guard other addition points such as Migrations / Authentication / CakephpFixtureFactories.

2-8. FormProtection の _Token not found (postLink + custom JS)

baserCMS uses its own JS (plugins/bc-admin-third/src/js/admin/common/lib/jquery.bcToken.js) and send it. In CakePHP 5.2, postLink's onclick changed to document.{form}.submit() → document.{form}.requestSubmit()(CSP-compatible), so the extraction regex no longer matches and _Token is no longer sent.

// jquery.bcToken.js: 両対応にする
var regex = /document\.(post_.+?)\.(?:request)?[Ss]ubmit\(\)/;
  • After fixing, rebuild the admin theme's JS: cd plugins/bc-admin-third && npm run build(bcToken is included in the common/startup bundle. webpack automatically entry-points src/**/*.js ).
  • Rather than excluding individual actions with unlockedActions , this regex fix canfix all postLink instances at once(CSRF protection is also maintained). Plugin disabling, theme switching/copying/deleting, cache clearing, etc. are all restored at once.

2-9. PluginCollection A change during live iteration causes subsequent plugins' bootstrap to be skipped

5.2 の BaseApplication::pluginBootstrap() は PluginCollection::with('bootstrap') の live generatoris traversed. If a certain plugin's bootstrap() changes the collection during iteration via addPlugin()/remove()(adding a theme, deleting DebugKit, etc.),the bootstrap() is skippedand,setting.php , etc. of plugins lined up immediately after are not loaded, and Configure::read('BcXxx....') becomes null.

  • In production, the problematic plugin (BaserCorePlugin in baserCMS) adds content plugins "later," so there is no element immediately after and the issue does not manifest, butif preloaded in tests (appPluginsToLoad equivalent), it manifestsdo.
  • Fix: stop live iteration, and until there are no unbootstrapped items,bootstrap per snapshot (test infrastructure BcTestCase::setUp , etc.).
    $booted = [];
    do {
        $pending = [];
        foreach ($app->getPlugins() as $plugin) {
            if ($plugin->isEnabled('bootstrap') && !in_array($plugin->getName(), $booted, true)) $pending[] = $plugin;
        }
        foreach ($pending as $plugin) { $booted[] = $plugin->getName(); $plugin->bootstrap($app); }
    } while ($pending);
    

2-10. View::_checkFilePath() second argument becomes the plugin name (MissingPluginException in templates containing.. )

5.2 で View::_checkFilePath(string $file, ?string $plugin) now passes $plugin を _paths($plugin) as the second argument._getTemplateFileName()/_getLayoutFileName() override andIf a filesystem path is passed as the second argument, the path is mistaken for a plugin name, and templates containing .. (such asrender('../element/...') ) return 500.

  • Fix: passnot a path but the plugin nameas the second argument._checkFilePath($path . $name, $path) → _checkFilePath($path . $name, $plugin)。
  • .. Normal templates that do not contain this are unaffected due to an early return (it only manifests via .. ).

2-11. TreeBehavior: at the root parent_id=null Setting and saving causes tree corruption

The TreeBehavior of 5.2, for the root (parent_id=0) entity parent_id=null If set and saved, it is treated as a "move to root" and lft/rght is recalculated. As a result, the tree is broken, and URL generation, etc. for descendants fails.

  • Fix: when updating the root, parent_id をdo not include it in the data(do not change it).
    $data = ['id' => $id, 'name' => $name, ...];
    if (!is_null($rootContentId)) $data['parent_id'] = $rootContentId; // ルート(null)では parent_id を触らない
    

2-12. Table::get() second argument also becomes a named argument (array options are deprecated)

find() As with Table::get($id, mixed ...$args) , in 5.2 it becomes

// Before → After
$table->get($id, ['contain' => ['Users']]);   →  $table->get($id, contain: ['Users']);
// 動的に options を組む場合はスプレッドで名前付き引数展開(PHP 8.1+)
$query->find($finder, array_merge(['prefix' => $prefix], $options));
  → $query->find($finder, ...array_merge(['prefix' => $prefix], $options));
  • Spread expansion assumesan associative array with string keys(matching the named argument names of finder/get).

2-13. Entity::set([...])(array bulk set) is deprecated

Setting an array to an entity in bulk uses patchEntity() .

// Before(テスト等)
$entity->set(['field' => $v, ...]);
// After
$table->patchEntity($entity, ['field' => $v, ...]);

2-14. Helper's real methods become magic method delegation → method_exists() が false(TextHelper::truncate , etc.)

With CakePHP updates, helper methods that were previously actually declared may be __call replaced by delegation via . Representative example: TextHelper::truncate/tail/highlight/excerpt/toList has no real method, andTextHelper::__call() が Cake\Utility\Text::{$method}() delegates to .

  • Impact: Code that checks existence with method_exists($helper, 'truncate') before callingmethod_exists() がonly makes actually declared methods return true, so it becomes false, and the relevant featuresilently stops working(no exception is raised, and null etc. is returned).
  • baserCMS real example: BcBaserHelper::__call() が PluginBaserHelper::methods() mapping ('truncateText' => ['Text','truncate']) was guarded before being called using method_exists() , andtruncateText() became unresponsive.
  • Fix: guard is_callable() change to .is_callable([$obj, $method]) returns true not only for real methods but also for objects that have __call (since the mapping is the source of truth for the call target, loosening the check still preserves design safety).
    // Before
    if (method_exists($pluginBaser->{$helper}, $target)) { ... }
    // After
    if (is_callable([$pluginBaser->{$helper}, $target])) { ... }
    
  • Detection: grep -rn "method_exists(" --include="*.php" scan for locations where the target can bea helper/utility delegation method.method_exists と is_callable The asymmetry of (only the latter makes __call return true) is the key point.
  • Test: BcBaserHelper Delegated methods via , if there is no enabled plugin, _initPluginBasers() returns early and BaserCore baser is not registered (PluginFactory::make(['name'=>'BcBlog'])->persist() verify after doing ).

Ecosystem: cakephp/migrations 4.5+ (bundled update during the CakePHP 5.2 period)

M-1. AbstractMigration / AbstractSeed deprecation of → BaseMigration / BaseSeed

From migrations 4.5 onward,Migrations\AbstractMigration / Migrations\AbstractSeed is deprecated. In tests that run migrations, Migrations\AbstractMigration is deprecated appears in large numbers (combined with xdebug stack dumps, it looks like a huge number of errors).

  • Fix: Change the base class to BaseMigration / BaseSeed .use Migrations\Table; → use Migrations\Db\Table;(table() match the return type Migrations\Db\Table ).
  • BaseMigration::__construct(int $version)(The argument is one int for version. Old AbstractMigration changed from multiple arguments).$this->input There is no .
  • Like baserCMS table() If you override to add a prefix,$this->input cannot be used, soobtain from the connection settings of the running adapterdo:
    public function table(string $tableName, array $options = []): \Migrations\Db\Table {
        $prefix = $this->getAdapter()->getConnection()->config()['prefix'] ?? '';
        return parent::table($prefix . $tableName, $options);
    }
    
  • Inline test migrations (within the test, $file->write('<?php class X extends AbstractMigration ...')) should also not be forgotten and changed to BaseMigration .

M-2. Migration test's new adapter API (Phinx\Db\Adapter\AdapterFactory abolished)

Old Phinx\Db\Adapter\AdapterFactory + Symfony Console の setInput() is incompatible with the new base. Use the new Migrations\Db\Adapter\AdapterFactory , and derive the adapter type fromthe connection's driver name(same as migrations core Environment::getAdapter() ). Avoiding hardcoding MySQL allows tests to pass on PostgreSQL/SQLite as well.

$connection = ConnectionManager::get('test');
$driverClass = get_class($connection->getDriver());
$adapterType = strtolower(substr($driverClass, (int)strrpos($driverClass, '\\') + 1)); // mysql / postgres / sqlite
$options = ['adapter' => $adapterType, 'connection' => $connection] + (array)$connection->config();
$adapter = \Migrations\Db\Adapter\AdapterFactory::instance()->getAdapter($adapterType, $options);

Version-independent (CakePHP 5.x in general / baserCMS-specific)

3-1. Controllers return a Response (direct output is prohibited)

header() + echo readfile() Direct output such as is headers already sent cause. Return a Response.

$content = file_get_contents($distPath);
// 後始末(unlink / emptyFolder)を先に実施
return $this->getResponse()
    ->withType('zip')
    ->withHeader('Cache-Control', 'no-store')
    ->withHeader('Content-Disposition', 'attachment; filename="' . basename($distPath) . '"')
    ->withStringBody($content);
  • Output such as KCAPTCHA is ob_start()/ob_get_clean() stringify with and withStringBody() return with .
  • Return type : void if you have added , change it to : \Cake\Http\Response .
  • Common in download-related features (theme DL, backup DL, log DL).

3-2. Class overriding and automatic detection tool (baserCMS-specific)

baserCMS overrides some CakePHP classes with plugins/baser-core/src/Routing/(namespace Cake\Routing) (Asset・RouteCollection etc.). AnalyseController scans the source and ReflectionClass when loading with , if you load with a path-derived class name (\BaserCore\Routing\Asset), it redeclares an already-loaded class in a different namespace (Cake\Routing\Asset) and causes a Fatal with Cannot redeclare class , stopping all tests midway. → AnalyseController::CONVERT_CLASS_NAME に '\BaserCore\Routing\Asset' => '\Cake\Routing\Asset' add a correction such as (Always register when you add overrides)。

3-3. Global env() A detector that depends on cannot be reproduced in integration tests → $request->getEnv()

When overriding the 'https' detector with reverse proxy detection etc.Global env() if you are looking at , integration tests ($this->configRequest() で environment injecting ) cannot reproduce it and the branch does not work.$request->getEnv() Using gives production-equivalent behavior and is reproducible in tests too.

// Before: $request->addDetector('https', fn() => env('HTTPS') === 'on' || ...);
// After:  $request->addDetector('https', fn($r) => $r->getEnv('HTTPS') === 'on' || ...);

3-4. Time comparison drifts during full test runs (long duration) → do not use real time() do not use

time()(real time) and the DB's modified A tolerance check comparing etc. (in tests, the Chronos-fixed bootstrap time) goes out of range when the full test suite takes tens of minutes,only during long runsfails.\Cake\I18n\DateTime::now()->getTimestamp() Using follows Chronos's fixed/shifted time, and in production it is identical to real time.

3-5. config/routes.php の fallbacks() hides content routing (404 in tests)

baserCMS resolves URLs with content routing (BcContentsRoute = /*) andBaserCorePlugin::disableRootRoutes() が config/routes.php の fallbacks()(/{controller}/{action}/*) is disabled. However, disableRootRoutes() は BcUtil::isConsole() でSkipped in the console (unit tests)so fallbacks remain only in tests and /{controller}/{action}/* が BcContentsRoute match before it, causing blog archives etc. to return 404.

  • Safety measure (current): config/routes.php の $builder->fallbacks(); をDisable(comment out) to match production behavior. Since baserCMS is primarily content-routing based and does not use generic controller fallbacks, this is the most robust.
  • ⚠️ Anti-pattern: The idea of "running disableRootRoutes() even in tests (isTest() permitting it with " isavoided。disableRootRoutes() destructively empties the route collection via reflection, but in the test environmentthe route collection accumulates across requests, so on the second and subsequent builds it wipes even API routes etc.,causing all API integration tests to return 404 (confirmed by actual measurement). Even a surgical removal that only deletes fallbacks tends not to offset the side effects of the wipe and easily breaks down.

How to proceed with the investigation (checklist)

  1. Whether it is fatal or a warningを logs/debug.log / logs/error.log determine by.
  2. When there are many failuresaggregate the exception messagesto identify the systemic cause.
  3. For each cause,app src originかtest/environment/fixture/i18n origindetermine which (git diff HEAD / Plugin::isLoaded / DI container registration / English↔Japanese messages).
  4. Fix app src-origin issues with the recipe in the relevant version section → php -l lint with → run the relevant test with --filter for unit verification.
  5. Finally, rerun all tests to confirm improvement in the count and whether there are any new regressions.

Related: PHP core-origin issues (implicit nullable, dynamic properties, fgetcsv, Reflection) go to the php-migration skill.