Skip to content

v2.13.0

Latest

Choose a tag to compare

@ttsuru ttsuru released this 23 Oct 03:57
· 31 commits to 2.x since this release
v2.13.0
6d4b141

Important

2.13.0 adds namespaces (Cake\ for the framework, App.namespace and the CakePHP 5 layout for applications and plugins) and keeps the global class names working. Before you update, check these points in UPGRADE.md:

  • A class that overrides a method that now takes variadic arguments (Model::query(), AuthComponent::allow(), ...) must declare the same parameters, or PHP stops when the class is loaded.
  • App::uses() is deprecated; the first call logs a warning.
  • Class names in logs, debug output and get_class() are the namespaced names; comparisons of class names as strings may need changes.
  • Some bug fixes change results (see "Bug Fixes That Change Results").
  • If you use pieceofcake2/phpstan-cakephp2, update it to ^1.1.

Breaking Changes: Variadic Arguments (PR #89)

  • func_get_args() replaced with variadic parameters: The methods that took a variable number of arguments declare them as ...$args, so that PHP and static analysis see them: Shell::dispatchShell(), AuthComponent::allow() / deny(), SecurityComponent::require*(), Controller::setAction() / validate() / validateErrors(), ContainableBehavior::contain(), DboSource::query(), Model::isUnique() / query(), CakeRequest::data() / param() / input() / allowMethod() / onlyAllow(), Router::parseExtensions(), CakeTestCase::loadFixtures(), Hash::merge(), Sanitize::stripTags(), Set::merge(), HtmlHelper::css() / useTag(). The calls do not change. A class that overrides one of these methods with the former signature fails with a fatal error: see UPGRADE

Enhancements: Plugins in the CakePHP 5 Layout (PR #88)

  • Plugin directories: Plugins may use the directories of the CakePHP 5 layout, as the application already does: src/ for the namespaced classes (src/Controller/, src/Model/, ..., loaded by Composer), templates/ for the views, resources/locales/ for the translations, vendor/ for the vendor files and config/ for bootstrap.php, routes.php, configuration files and schemas. App::path() and I18n search them, when they exist, before the directories of the CakePHP 2 layout (Controller/, View/, Locale/, Vendor/), which are still searched. App::uses() and App::load() keep loading global classes from the CakePHP 2 directories
  • CakePlugin::configPath(): A new method returns the config/ directory of a plugin, or Config/ when the plugin has no config/; CakePlugin::bootstrap(), CakePlugin::routes(), PhpReader, IniReader and CakeSchema use it

Enhancements: Templates in the CakePHP 5 Style (PR #94)

  • .php templates: In templates/ directories (of the application, of plugins, of themes), templates, layouts and elements may use the .php extension of CakePHP 5. In each directory a .php template comes before the .ctp template of the same name, and the order of the directories is unchanged, so themes and plugins still override the templates of the application. The View/ directories of the CakePHP 2 layout, which also hold the view and helper classes, are not searched for .php templates
  • CakePHP 5 directory names: In templates/ directories, layout/, element/, email/ and Error/ are searched before Layouts/, Elements/, Emails/ and Errors/. Email templates may be in templates/email/html/ and templates/email/text/, and their layouts in templates/layout/email/html/ and templates/layout/email/text/

Enhancements: Classes in a Namespace (App::className(), App.namespace) (PR #91)

  • App::className(): A new method, backported from CakePHP 3 and later, returns the class name for a name used in the configuration, or null. A name with a backslash is a fully qualified class name; the suffix is added only when the name does not end with it, so GreetingComponent::class and 'Acme\Controller\Component\Greeting' both return Acme\Controller\Component\GreetingComponent. Other names are looked up in the namespace of the plugin or of App.namespace (<Namespace>\<Type>\<Name><Suffix>), then as the global class declared with App::uses(), as before
  • Fully qualified class names: Cache engines, email transports, AuthComponent authenticate/authorize objects, password hashers, view classes (Controller::$viewClass, CakeEmail::viewRender(), RequestHandlerComponent::viewClassMap()), components, helpers, behaviors, shell tasks, shell helpers and the PaginatorHelper ajax helper use App::className(). They accept Foo::class, where the suffix was added a second time before (MemoryEngine::class looked for MemoryEngineEngine)
  • App.namespace: When Configure::write('App.namespace', 'App') is set, controllers (Dispatcher, ControllerTestCase::generate()), shells (ShellDispatcher), models (ClassRegistry::init(), $uses, CakeTestCase::getMockForModel()) and the classes above are also found by their short name in that namespace, e.g. App\Controller\PostsController for /posts, before the global classes. A controller in the namespace merges $components and $helpers with the AppController of the namespace
  • The other class settings: Log engines, the exception renderer (Exception.renderer, also in IntegrationTestTrait), datasources, session handlers, route classes ('routeClass'), dispatcher filters, Acl.classname, the engine of TextHelper, NumberHelper and TimeHelper, the readers of HtmlHelper::loadConfig(), the localized classes of Validation (NlValidation), the response class and authentication methods of HttpSocket, test reporters, the models of fixture imports, CakeSchema and the ExtractTask, and the shells of CommandTask also use App::className(), as on the feature/phpstan-level5 branch. Their global names and their fully qualified names work as before, and they are also found in App.namespace and in the namespace of a plugin
  • Plugin AppController and AppModel: A plugin controller merges the vars of the AppController of the namespace of its plugin (MyPlugin\Controller\MyPluginAppController), ClassRegistry::init() uses the AppModel of that namespace for a plugin model without a class, and a model in App.namespace merges $actsAs and $findMethods with App\Model\AppModel
  • Names without the namespace: Controller::$name, Model::$name and Shell::$name are derived from the class name without its namespace, e.g. Posts for App\Controller\PostsController
  • Namespaced fixtures: Fixtures are also loaded from Cake\Test\Fixture (core.*), <App.namespace>\Test\Fixture (app.*) and <Plugin>\Test\Fixture (plugin.*), before the global classes; the name and the table come from the class name without the namespace
  • App::import() and App::load(): App::import() also finds namespaced classes, App::load() includes each file once, and an application with AppController, AppModel, AppHelper or AppShell only in App.namespace gets the global name for it, so that core classes such as CakeErrorController that extend it work
  • AppController in any namespace: A controller merges the vars of a parent class named AppController in any namespace, also without App.namespace
  • ClassRegistry::init(Foo::class): A fully qualified class name without an alias gets the short class name as its alias, e.g. Post for ClassRegistry::init(Post::class), as a short name does; before, the alias contained the backslashes
  • ClassRegistry keys: ClassRegistry::getObject(), isKeySet() and removeObject() also find an object stored with the short name of its class by the fully qualified class name, e.g. ClassRegistry::getObject(TranslateBehavior::class) or ClassRegistry::getObject(Post::class) for the model with the alias Post, and otherwise the only object of exactly that class, e.g. a Post with the alias Article; with several objects of the class nothing is found
  • Class names as aliases: Fully qualified class names as keys of $components, $helpers, $actsAs, Shell $tasks, $uses (Controller and Shell) and the model associations, or given to load() of the collections and loadModel(), get the short class name without the namespace and the suffix as alias and property name, e.g. public $components = [GreetingComponent::class] gives $this->Greeting, $uses = [Post::class] gives $this->Post and $modelClass Post, and $belongsTo = [Author::class] gives the association Author with className Author::class, the same as the short name with className. A class name key with its own className also gets the short alias and keeps that className; before, the alias contained the backslashes. enable(), disable(), unload(), loaded(), enabled() and set() of the collections and bindModel()/unbindModel() also take the class name
  • Merged class names: When the vars of a parent class are merged ($components, $helpers, $actsAs, Shell $tasks, also from a plugin AppController and the AppModel), a class name key is replaced by its alias with the class name in 'className', so 'Greeting' => [...] in the AppController and GreetingComponent::class => [...] in a controller give one entry Greeting with the merged settings, as two short names do. The new ObjectCollection::normalizeClassNames() does this
  • Join models by class name: 'with' => PostsTag::class of a hasAndBelongsToMany association works like 'with' => 'PostsTag' when reading, saving, validating and deleting (joinModel(), __isset(), _saveMulti(), _deleteLinks()), and in CakeSchema::read(); the scaffold names the controller of an association with a class name as className by the short name
  • Missing classes: The errors for a missing component, helper, behavior, task or controller (ControllerTestCase::generate()) add the suffix to a fully qualified class name only when it does not end with it, e.g. Acme\Controller\Component\GreetingComponent instead of ...GreetingComponentComponent
  • Cached views: Controller::$controllerClass and View::$controllerClass hold the class name of the controller, and the views cached by CacheHelper create a namespaced controller by that name
  • Fixes for namespaced classes: Dispatcher filters with settings, CakeSchema::read() for plugin models, ClassRegistry::init() for a stored alias, the plugin and name of namespaced models in the rendered view, and the plugin AppController of a request whose plugin is not loaded (no MissingPluginException)

Enhancements: The Framework in the Cake Namespace (PR #92)

  • Namespaced classes: The classes of the framework are in the Cake namespace, following their directories: Cake\Controller\Controller, Cake\Network\CakeRequest, Cake\Utility\Hash, ... Applications and plugins can use them, e.g. use Cake\Controller\Component;
  • src/ and PSR-4: As on the feature/phpstan-level5 branch, the classes are in src/ (src/Controller/Controller.php) instead of src/Cake/, and Composer loads them with "psr-4": {"Cake\\": "src/"} instead of a classmap. The files that declared several classes are split into one file per class (src/Error/NotFoundException.php, src/Controller/Component/Acl/PhpAco.php, src/TestSuite/ControllerTestDispatcher.php, ...). CAKE is src/. src/Cake/bootstrap.php and src/Cake/TestSuite/Fixture/CakeFixtureInjector.php stay at their former paths for the applications and plugins that name them
  • Global names: The global names of CakePHP 2 keep working as aliases of the same classes (class_alias()). Each class file declares its alias when the class is loaded, and src/aliases.php, loaded by Composer, loads the namespaced class when an application uses the global name first. Controller and Cake\Controller\Controller are the same class: extends, instanceof, type declarations, catch blocks and App::uses() work with either name
  • Class names in output: get_class(), ::class and the messages, logs, debug output and error pages that show them use the namespaced names, e.g. [Cake\Error\NotFoundException] in the error log
  • Overriding core classes: A global class registered with App::uses() no longer replaces a core class, as the core uses the namespaced names. Override a core class with a copy in src/Cake/ (e.g. src/Cake/Routing/Dispatcher.php) loaded by the classmap of the application, and exclude the core file with exclude-from-classmap
  • phpcs: SlevomatCodingStandard.Namespaces.UnusedUses counts the class names in annotations, as the core imports the classes that its docblocks name

Enhancements: Fixture Class Names (PR #108)

  • Fixtures by class name: A fixture is preferably named by its fully qualified class name, e.g. public $fixtures = [ArticleFixture::class] and $this->loadFixtures(ArticleFixture::class), as the other classes in the configuration are. $fixtures, loadFixtures() and CakeFixtureManager::loadSingle() accept it and use exactly that class, so that fixtures with the same name in different namespaces do not collide; loadSingle() throws UnexpectedValueException for a class that is not among the fixtures of the test case. A leading backslash is ignored, and the Fixture suffix is added only when the name does not end with it. A class given more than once, also in another notation, has one instance, so that its table is created and dropped once. The former notation, core.article, app.article, plugin.foo.bar and Article, works as before

Deprecations: App::uses() (PR #98)

  • App::uses(): Deprecated. Composer loads the classes of the core, of pieceofcake2/app and of the plugins installed with it, and the core finds the classes named in the configuration without it. The first call in a process logs a warning with the file and line of the call, and triggers E_USER_DEPRECATED when debug is above 0. The call still works
  • Core: The core no longer calls App::uses(). AppController, AppModel, AppHelper and AppShell of the application are found in their packages without it, and the classes that the core loads from an application or a plugin are found with App::className(). The code that CacheHelper writes into cached views and the code that the error pages suggest no longer contain it

Bug Fixes: PHP Deprecations (PR #96)

  • Null passed to built-in functions: Many methods passed a value that can be null (e.g. env() of a missing variable, an unset option, the plugin of pluginSplit()) to a string function of PHP, which is deprecated since PHP 8.1. They cast it to a string (or an int for the decimals of CakeNumber), so the result is the same as before. This covers among others CakeSession, CakeRequest, CakeResponse, CakeEmail, HttpSocket, Security, Configure::consume(), Xml::build(), FormHelper date and radio inputs, CakeText::cleanInsert(), Hash / Set matching, File, pluginSplit(), env(), clearCache() and the missing class exceptions of the object collections
  • False converted to an array: I18n (on a cache miss of a domain) and CakeRoute (with named parameters before Router::connectNamed()) no longer add keys to false, deprecated since PHP 8.1
  • Xml: Xml::build() no longer calls libxml_disable_entity_loader(), deprecated since PHP 8.0. External entities stay disabled: libxml 2.9 and later, which PHP 8 requires, does not load them without LIBXML_NOENT, which Xml does not pass
  • Xml loadEntities option removed: The option of Xml::build() only decided whether to call libxml_disable_entity_loader(), so it no longer has any effect and is removed. Passing it is ignored
  • Security: Security::hash() with sha256 always uses hash() instead of mhash(), deprecated since PHP 8.1. The digest is the same
  • SessionHandlerAdapter: The methods of SessionHandlerInterface have #[\ReturnTypeWillChange], so PHP 8.1 and later no longer report their missing return types
  • CakeTestFixture: CakeTestFixture declares $import, so setting it on a fixture that does not declare it no longer creates a dynamic property, deprecated since PHP 8.2
  • Null as an array key: I18n, Helper::setEntity(), FormHelper, RequestHandlerComponent::respondAs(), CakeResponse::getMimeType(), CakeSession::check(), Configure::consume(), ClassRegistry::getObject(), ConsoleOptionParser::parse() / help() and Set::extract() check that a name or key is given before they use it as an array key, instead of using null, deprecated since PHP 8.5. A missing name finds nothing, as before. I18n looks up the translations without a context under the empty context, where they are stored
  • Reflection: Debugger::export() calls ReflectionProperty::setAccessible() only before PHP 8.1, where it has an effect. It is deprecated since PHP 8.5
  • No request input or URL: CakeRequest::input() with a decoding callback returns null without calling it when the request has no body, or when _readInput() gives null, e.g. in a subclass or a mock, instead of passing the empty input on. RequestHandlerComponent sets the same empty data as before. A callback of your own that handled an empty body now gets no call. CacheHelper writes no cache file when here() gives no URL, instead of passing null to the string functions of PHP
  • Mysql: The Mysql datasource uses the Pdo\Mysql::ATTR_* constants instead of PDO::MYSQL_ATTR_*, deprecated since PHP 8.5. The core requires symfony/polyfill-php84, which defines Pdo\Mysql with the same values before PHP 8.4
  • RedisEngine: RedisEngine::delete() deletes the key with Redis::del() instead of Redis::delete(), which phpredis deprecates
  • Security: Security::encrypt() encrypts null as the empty string, instead of passing it to strlen() (deprecated since PHP 8.1)
  • Sqlserver: Sqlserver::limit() checks that the offset is a string before ctype_digit(), renderStatement() checks that a limit is given before it looks for TOP or FETCH, and create() checks that the table has an identity column before it looks for its value, instead of passing null or other types to them (deprecated since PHP 8.1)

Bug Fixes: Null Direction in DboSource::order() (PR #101)

  • DboSource::order(): A field without a direction (e.g. order('Post.title', null)) no longer passes null to trim(), deprecated since PHP 8.1. The SQL is unchanged
  • Tests: The test of the directions passes the errors other than E_USER_DEPRECATED to PHPUnit, so it reports deprecations again

Bug Fixes: Console (PR #104)

  • ShellDispatcher: An unknown shell gives MissingShellException, not MissingPluginException
  • AclShell: The parser and help no longer set a dynamic property (the unsupported default of the action arguments is removed)
  • AclShell: nodeExists aco <number> uses foreign_key, not the missing object_id column
  • AclShell: A Model.foreign_key reference that is not found is reported with "Could not find node using reference" instead of an uncaught exception
  • ProgressShellHelper: draw() passes an integer to str_repeat(), as a float is deprecated since PHP 8.1
  • TestShell: A file in src/ of a plugin maps to its test case in tests/TestCase/
  • TestShell: The "cannot be run via this shell" message names the given file
  • ApiShell: The input l and other text that is not a number no longer fail on PHP 8.3 and later
  • ApiShell: Each lookup reads the classes of its own path, not the cached list of the first one
  • ApiShell: A class file that cannot be included is reported without a ReflectionException
  • ApiShell: api help className and api help type show their help
  • ApiShell: api helper Html finds HtmlHelper; the type suffix is added before the lookup
  • ConsoleShell: unbind with an unknown model prints the "Please verify" message
  • ConsoleShell: bind and unbind use the stripped names
  • File: Creating a file whose directory cannot be created no longer passes null to pathinfo() and is_dir()

Bug Fixes: Found by the Tests (PR #107)

  • Test runner page: The HTML reporter of test.php escapes the request values that it writes into the page (case, plugin, filter and the links) and the pages escape the base directory taken from PHP_SELF, which could run a script in the page (XSS). Invalid UTF-8 in them is replaced instead of being dropped. The page header has its doctype, <html>, <head>, charset and title again, and the pages for PHPUnit, Xdebug and a missing connection get the base directory for their stylesheet and images
  • Test reporters: Skipped and incomplete tests, and tests with a warning, are no longer painted as passes. A failure shows the exception that the test threw instead of the wrapper of PHPUnit, with its class, file, line and trace, and the text reporter shows the line of the test that failed, also when the test throws it itself or runs in another test. The case list of the text reporter links plugin and core cases with &plugin= and &core=true and encoded names, and prints an empty list once instead of twice. The summaries count only the finished tests as complete and show the skipped and incomplete tests: see UPGRADE
  • Test runner: A core.* fixture is looked up only in the fixture path of the core. A test case of a plugin that does not exist raises MissingPluginException instead of looking for the file under /
  • JsHelper engines: escape() writes a character outside the Basic Multilingual Plane as a UTF-16 surrogate pair, other C0 control characters as \u00XX instead of dropping them, and a question mark for bytes that are not valid UTF-8 (a stray continuation byte, a sequence cut off by the end of the string or by another byte, an overlong sequence, a surrogate, the lead bytes 0xF8 to 0xFF). value() writes a float with a dot whatever the locale. object() replaces invalid UTF-8 with U+FFFD instead of returning only the prefix and postfix. object() and value() throw CakeException for INF, NAN and other data that json_encode() cannot encode. redirect() escapes the URL in its JavaScript string. The query string of the data of request() writes nested arrays as key[sub]=value instead of Array, leaves a null value out and writes false as 0, as Router does; the values are still not URL-encoded. Empty callbacks are left out instead of giving error:
  • Scaffold: The list action is removed. It was the former name of index, kept as an alias when the views were renamed in CakePHP 1.2, but there was no list view, so it failed with MissingViewException. A request to list now gives MissingActionException, as other actions that are not scaffolded do. create and update stay
  • CakeSocket: A fractional timeout is passed to stream_set_timeout() as seconds and microseconds. Through a proxy, the CONNECT request sends the target as Host, and a status other than 2xx throws SocketException. reset() closes the open connection before it sets a state with a connection. The connection warnings are cleared at each connect()
  • CakeSession: clear() empties the session data before it renews the session, so that the data is not carried over to the new id. In debug mode, the save path of Session.ini session.save_path is created for the files handler, also in the N;path form; nothing is created for other handlers
  • DataSource: insertQueryData() accepts an integer 0 as the id, and throws NotImplementedException when the datasource has no value() method
  • Model: TreeBehavior::reorder() stops on a tree that is not valid. DboSource::create() and Sqlserver::create() without fields insert the data of the model. getColumnTypes() of a model without a schema no longer raises a PHP warning from foreach() besides its own warning. Sqlserver::lastAffected() returns the rows changed by an UPDATE or a DELETE instead of 0
  • CakeSchema: write() without tables writes the tables that read() finds
  • Folder: realpath() resolves a relative path with .. from the current directory, so cd('../sibling') works
  • Debugger: excerpt() highlights the given line in the middle of the lines it returns: see UPGRADE
  • Translations of "0": __() and the other translation functions translate the message '0' instead of returning null, and a translation whose msgstr is "0" is used. Po files load the messages "0" and other numbers, which were dropped or stored under another number
  • Multibyte: utf8() and ascii() decode and encode the characters outside the Basic Multilingual Plane (4 bytes in UTF-8), so strtolower(), strtoupper() and the other methods keep them instead of breaking them

Bug Fixes: AclNode::node() of a Number (PR #110)

  • AclNode::node(): A reference that is not an array, a path or a model, such as the number that AclShell passes for a numeric argument (cake acl check 5 3), finds nothing again, as before v2.12.6, instead of failing with a TypeError

Bug Fixes: The Key '0' (PR #111)

  • The key '0': Configure::consume(), CakeResponse::getMimeType(), ClassRegistry::getObject() and the subcommands of ConsoleOptionParser take '0' as a key again. The checks for a missing key of PR #96 used empty(), which also rejected '0'; they now check for null only

Bug Fixes: Found by the Tests of the Namespace Pull Requests (PR #112)

  • PhpAcl: check() of an ACO without nodes, such as '/', returns the policy instead of raising "Undefined array key 0"; PhpAco::path() returns an empty path for it
  • ControllerTestCase::$loadRoutes: false resets the routes again before testAction() parses the URL, so the routes that the test connected are not used. The Dispatcher has not called ControllerTestDispatcher::_loadRoutes() since the routes are loaded lazily (CakePHP 2.3); ControllerTestDispatcher::parseParams() now calls it
  • Cache::drop(): The dropped configuration is removed from its groups, so Cache::groupConfigs() no longer lists it, and a group that no configuration uses any more is removed
  • Cache::config(): An engine whose init() fails is not kept, so configuring it again with the same settings throws the CacheException again instead of using the engine that failed
  • AppShell: The shells and tasks of the framework extend the global AppShell again, which is the AppShell of the application when it has one, as before PR #92. Cake\Console\Command\AppShell no longer declares the global name itself; src/aliases.php declares it only for an application without an AppShell. Loading a shell of the framework after the AppShell of the application no longer fails with "Cannot redeclare class AppShell", and loading Cake\Console\Command\AppShell first no longer hides the AppShell of the application

Tests: Coverage (PR #106)

  • More tests: Tests for the test suite (reporters, runner, fixture manager), the Js helpers, the scaffold, the network and session classes, the datasources, the models and behaviors, CakeSchema and the utilities. The bugs that they found are fixed in PR #107

Tests: Patch Coverage and Repeated Runs (PR #112)

  • Coverage of PR #92 and PR #98: Tests for the global class names in a new process, the former paths in src/Cake/, CakeFixtureInjector, PhpAcl, MissingConnectionException, ControllerTestCase without autoMock and the cached views of global and plugin controllers. The bugs that they found are fixed above
  • Repeated runs: The tests restore the state that they change, so they also pass with --repeat 2 or alone; the tests that need classes that are not loaded yet, or define a constant, run in their own process
  • Folder sort by time: The tests of Folder::SORT_TIME wait until the files have later change times, instead of sleep(1)

Tests (PR #96)

  • Deprecations reported: The tests report deprecations and fail on them. ConfigureTest::testBootstrap() restores the error level after Configure::bootstrap(), which had hidden deprecations from the following tests, and the tests no longer create dynamic properties, or use utf8_decode(), new DateTime(null), str_getcsv() without the escape character or session.referer_check. The DATABASE_CONFIG of the tests allows dynamic properties, which ConnectionManager::create() adds. The test helpers call setAccessible() of reflection only before PHP 8.1. ApplicationControllerTest tests the redirect without cookies on every version, and the session ID in the URL, which needs session.use_trans_sid deprecated since PHP 8.4, only before PHP 8.4. The tests that use mcrypt check that it reports the deprecation of its functions, and MemcacheEngineTest replaces Memcache::set() with a small subclass, as a mock would copy the implicitly nullable parameters of the extension (deprecated since PHP 8.4)

Tests: Error Handler of the Test Bootstrap (PR #103)

  • Application tests follow their phpunit.xml: The test bootstrap no longer turns every deprecation into an exception in the tests of applications

Tests: Namespaced Tests and AllLegacyTests (PR #95)

  • Namespaced tests: tests/ holds the namespaced tests of feature/phpstan-level5 (Cake\Test\TestCase\...), with a test application in the CakePHP 5 layout (tests/test_app/, TestApp\). The classes of the application are real classes of the test application, not aliases, and the tests that need it set App.namespace in setUp()
  • AllLegacyTests: A second test suite, run with vendor/bin/phpunit --testsuite AllLegacyTests, checks that an application in the CakePHP 2 layout (tests/test_legacy_app/) and plugins in the legacy layout keep working with the 2.x notation and without App.namespace, and that a namespaced plugin works in it and a legacy plugin in a namespaced application
  • Test plugins: The test plugins are named Test<Name>Plugin, and Test<Name>LegacyPlugin in the legacy layout

Upgrading

composer require pieceofcake2/cakephp:^2.13
composer require --dev pieceofcake2/phpstan-cakephp2:^1.1   # if you use it

See UPGRADE.md, the migration guide, Migrating to the Modern Layout and Autoloading. pieceofcake2/upgrade moves an application or a plugin to the new layout and to namespaces.

Full Changelog: v2.12.11...v2.13.0