A collection of deprecations and breaking changes encountered during PHP version upgrades, along with fix recipes.Organize and grow thisby version. For issues caused by the CakePHP framework itself, see the separate skill cakephp-migration .
Included: PHP 8.2 / 8.4 / 8.5. When supporting a new version, append a new "## PHP X.Y" section. Note: PHP 8.5 is the version immediately before 9.0.All deprecated features are removed in 9.0, so deprecation warnings that appear in 8.5 should be eliminated early.
General Principles
- Distinguish between "fatal errors" and "deprecation warnings". In debug mode, deprecation warnings are displayed at the bottom of the screen and look like "errors," but those emitted with
logs/debug.logにdebug:are warnings (operation continues). Fatal/Exception are the real errors. - Separate regressions from pre-existing failures。
git diff HEAD -- <file>to check whether the file in question is within your own changes. - If you change a parent class signature,align the child classes and interfaces as well。
- Static analysis such as Rector is effective for exhaustive detection of all deprecations.
For how to run unit tests, syntax checks, and how to isolate failures, see the
basercms-unittestskill.php -l(syntax check) also emits deprecation warnings such as implicit nullable.
PHP 8.2
Deprecation of Dynamic Properties
Assigning to undeclared properties is deprecated.Creation of dynamic property X::$prop is deprecated。
- Measure A (recommended): Declare the property on the class.
※ In the constructor,/** @var ThemeFoldersServiceInterface */ protected $ThemeFoldersService; protected $_tempalteTypes = [];$this->Xxx = $this->getService(...)is a typical place where such assignment occurs. - Measure B: For base classes that affect many child classes (e.g., the test base
BcTestCasehas over 400 subclasses), apply#[\AllowDynamicProperties].The attribute is inherited by child classes, so one place is enough.#[\AllowDynamicProperties] class BcTestCase extends TestCase { ... }
PHP 8.4
1. Deprecation of Implicitly Nullable Arguments
型 $x = null is implicitly nullable and deprecated. Explicitly ?型 to .
// Before
function foo(int $contentId = null, BcAbstractDetector $agent = null)
// After
function foo(?int $contentId = null, ?BcAbstractDetector $agent = null)
- Detection (regex): Matches arguments that do not have
?at the beginning and are型 $name = null. Example:([A-Za-z_\\][A-Za-z0-9_\\]*)\s+(\$[A-Za-z0-9_]+)\s*=\s*null(those without?immediately before). - When changing a parent class signature, align child classes/interfaces as well.
- Warning:
Implicitly marking parameter $x as nullable is deprecated, the explicit nullable type must be used instead。
2. fgetcsv() の $escape deprecated
$escape Omitting the argument is deprecated. Specify it explicitly.
// Before
$head = fgetcsv($fp, 10240);
// After
$head = fgetcsv($fp, 10240, ',', '"', '\\');
- Custom implementations (
fgetsfunctions that parse CSV with +regex, etc.) do not usefgetcsv, so they are out of scope. Fix only the places that actually callfgetcsv().
3. ReflectionProperty::setValue() deprecation of single-argument calls
For non-static properties, pass the target object.
// Before
$property->setValue($value);
// After
$property->setValue($object, $value);
- Warning:
Calling ReflectionProperty::setValue() with a single argument is deprecated。 - It tends to lurk in test utilities (helpers for request generation and private property manipulation).
- For static properties, pass
nullas the first argument:setValue(null, $value). This is Valid in all of 8.1–8.5(does not break 8.1 compatibility).
4. Passing string to the null argument of internal functions is deprecated (actual entity is 8.1+)
parse_url(null) / version_compare($a, null) / str_replace(..., null) etc., passing?string to a non- null argument causes an explicit conversion in Passing null to parameter #N ($x) of type string is deprecated。(string) cast.
// Before → After
parse_url($url); → parse_url((string)$url);
version_compare($current, $required); → version_compare((string)$current, (string)$required);
- Although deprecated and introduced in 8.1, it surfaces in large numbers during migration work (especially updates to 8.4/8.5), so it is also included in this document.
- Because it only appears at runtime when the value is null,static grep cannot catch it all. Aggregate the
Passing null to parameterfrom all test logs to identify the source.
PHP 8.5 (released 2025-11-20)
The final minor version just before 9.0. Most of the following are warnings and operation continues, but they will be removed in 9.0, so address them. Listed in order of likely impact on apps/frameworks.* Only "0." is not a warning but a fatal error (breaking change), so it has the highest priority.
⚠️ Precautions while maintaining PHP 8.1 compatibility(
composer.jsonは"php": ">=8.1"). If you fix 8.5 deprecations with the "recommended replacement", there are cases where the replacement does not exist in 8.1 and 8.1 stops working with a Fatal error. The ❌ marks below indicate defer until you drop 8.1(warnings will appear in 8.5 but are tolerated). Running on 8.5 itself does not break 8.1 (warnings only).
- ❌
str_increment()(8.3+) → in 8.1,$s++remains- ❌
http_get_last_response_headers()(8.4+) → in 8.1,$http_response_headerremains- ❌ Namespaced PDO (
Pdo\Mysql::ATTR_*etc., 8.4+) → in 8.1,PDO::MYSQL_ATTR_*remainsOther than the above (non-canonical casts,
nulloffset →'', removal of auto-release functions, backquotes →shell_exec、case :、__serialize)は Also safe in 8.1can be applied.
0. [Breaking change / fatal] Referencing reserved class names (Array etc.) with use causes a Fatal error
8.5 で Array etc. are reserved as special class names, resulting inuse ...\Array; や class Array が Fatal(not a warning). If it fails at the test collection (autoload) stage,the entire suite cannot start, so it has the highest priority.
Fatal error: Cannot use BcMail\Test\TestCase\Model\Array as Array because 'Array' is a special class name
- baserCMS track record:
MailMessagesTableTest.phpAt the beginning, remnants from the CakePHP2 erause ...\Array;/use ...\ClassRegistry;(all dead imports to nonexistent classes) remain, which were harmless up to 8.4 but became Fatal in 8.5.Delete the correspondinguselineto resolve it (the reference locations aremarkTestIncompleteunexecuted code). - Detection:
grep -rnE "use [A-Za-z0-9_\\\\]+\\\\(Array|Object|Resource|Enum|Mixed|Never|Void|Null|False|True|Iterable|Numeric)\s*;" --include="*.php"。 - Note: Because it is fatal, it can be detected per file with
php -l. However, cleaning it up with grep before all tests can prevent accidents where the suite fails at the collection stage.
1. Deprecation of non-canonical cast names (high impact)
(boolean) (integer) (double) (binary) is deprecated. Replace it with the canonical name.
// Before → After
(boolean) → (bool)
(integer) → (int)
(double) → (float)
(binary) → (string)
- Detection:
grep -rnE "\((boolean|integer|double|binary)\)" --include="*.php"。 - Often remains in old code. Can be replaced mechanically.
2. null using it as an array offset is deprecated
$arr[null] / array_key_exists(null, $arr) etc., using null as a key is deprecated (it was implicitly converted to "" ). Explicitly use the empty string "" .
// Before
$arr[null] = $v; array_key_exists(null, $arr);
// After
$arr[''] = $v; array_key_exists('', $arr);
- Be careful where a variable becomes a key (
$arr[$key]で$keycan be null). - Difficult to detect with static grep(it only appears when the value is null at runtime). Run all tests on 8.5 and aggregate the origin (
grep "null as an array offset" phpunit.log) withon line N of <file>to be sure. - Fix policy: ① Use
$key ?? ''for the key in assignments/references (equivalent to the previous implicit conversion.Make both set/get use the same key), ② Receiving null returns such asisset()before the / reference with$key !== null &&guard, or any of these.getData('batch')is also effective.?? ''. - baserCMS track record (308 total):
BcFileUploader(uploadingFiles[$bcUploadId]の set/get を?? ''consistent),BcUploadBehavior(oldEntity[$alias][$entity->_bc_upload_id]を?? '')、MailMessagesTable($dists[$mailField->group_valid])、SearchIndexes/Contents/PermissionsController(getData('batch') ?? '')、BcTextHelper::arrayValue/BcUtil::decodeContent($key !== null && isset(...))。
3. Deprecation of resource-freeing functions that automatically free objects
The target is no longer a resource but an object, and since it is automatically freed by GC, explicit close/free calls are deprecated.Delete the calljust do it.
curl_close()/curl_share_close()(CurlHandle / CurlShareHandle)finfo_close()(finfo)imagedestroy()(GdImage) ← often used in image processing utilitiesxml_parser_free()(XMLParser)- Detection:
grep -rnE "\b(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free)\s*\(" --include="*.php"。 - baserCMS track record:
Imageresizer.php・BcFileUploader.phpのimagedestroy()4 locations. Simple deletion is fine, but to preserve the intent of immediately freeing memory inside the loop, replace withunset($img)(GdImage is an object, so GC frees it once the reference is removed. Safe on >=8.1)..
3-2. ReflectionProperty/Method::setAccessible() deprecation (highest occurrence count)
setAccessible() は No-op since PHP 8.1(private/protected も getValue/setValue/invoke can be accessed directly). Deprecated in 8.5.Delete the calling line(safe on >=8.1).
// Before
$property = new ReflectionProperty($obj, 'foo');
$property->setAccessible(true); // ← この行を削除
$property->setValue($obj, $v);
- baserCMS track record: 33 locations including src/tests (
BcUtil/BcTestCase/BaserCorePlugin/PreviewController/BcDatabaseServiceand others). Because it is on hot paths (such as the test base),the runtime occurrence count is orders of magnitude higher(about 6,300 across all tests ≒ the majority of 8.5 deprecations). Clearing this first will drastically reduce the logs. - Bulk deletion(limited to standalone statements):
sed -i -E "/^[[:space:]]*\$[A-Za-z0-9_]+->setAccessible\(true\);[[:space:]]*$/d" <file>。 - Caution when detecting: set the grep pattern to
->setAccessibleas-When starting withgrep misinterprets it as an option and returns 0 results.grep -rn "setAccessible"as-Avoid starting with it (orgrep -e)。
4. Deprecation of incrementing non-numeric strings
$str++(non-numeric string) is deprecated.str_increment() Use※ str_increment() . $str++ .
// Before
$code++; // $code が 'AZ' 等
// After
$code = str_increment($code);
5. __sleep() / __wakeup() soft deprecation
__serialize() / __unserialize() Use it (migrate if PHP7 compatibility is not needed).
6. $http_response_header deprecation
Superglobal $http_response_header is deprecated.http_get_last_response_headers() Use※ http_get_last_response_headers() . $http_response_header .
7. Deprecation of the backtick operator
`command`(shell_exec() alias) is deprecated.shell_exec() directly.
8. case statement with ; is deprecated.
case 1; is deprecated.case 1: to .
- Detection:
grep -rnE "case\s+.+;\s*$" --include="*.php"(visual confirmation required due to many false positives).
8-2. array_key_exists(null, ...) deprecation (triggered by malformed serialize)
array_key_exists() first argument null is deprecated (use an empty string).Framework internal (CakePHP JsonView::_dataToSerialize), the cause is your own code's serialize option malformed。setOption('serialize', ['foo', 'message' => $message]) asMixing 'key' => 値 into the variable name listと、$message is null, 'message' => null gets into the serialize array, andarray_key_exists(null, ...) runs.
- Fix: serialize はList of view variable namesto .
['foo', 'message' => $message]→['foo', 'message'](values already$this->set()). baserCMS track record:MailMessagesControllerindex/view actions. - How to pinpoint when the origin is inside the framework: temporarily instrument vendor (
if ($key === null) file_put_contents(..., var_export($serialize,true).getTraceAsString())) → run the relevant plugin's tests → identify the first in-house controller in the stack trace → restore vendor.
8-3. ArrayObject passing an object as the backing array is deprecated
new ArrayObject($obj) でObject(json_decode() stdClass etc.) is deprecated. Cast to an array.
- Fix:
new ArrayObject(json_decode($body)->devices)→new ArrayObject((array)json_decode($body)->devices). baserCMS track record:SitesControllerTest。
9. Other (only if applicable)
- PDO driver-specific constants/methodshave moved to namespaced (
PDO::MYSQL_ATTR_*→Pdo\Mysql::ATTR_*、PDO::sqliteCreateFunction()→Pdo\Sqlite::createFunction()etc.). Most are internal to the framework, but if you use DB driver constants yourself, handle it. PDO's"uri:"DSN scheme is also deprecated.※ Namespaced PDO classes are 8.4+. While maintaining 8.1, leave it asPDO::MYSQL_ATTR_*. openssl_pkey_derive()の$key_length、finfo_buffer()の$contextargument is deprecated (ignored).- In non-CLI SAPI,
register_argc_argvINI is deprecated (use$_GET/$_SERVER['QUERY_STRING']). - Some closure bindings such as binding to static closures are deprecated.
clone nullerror message change (test impact): up to 8.4,__clone method called on non-object、8.5 はclone(): Argument #1 ($object) must be of type object, null given。null をcloneTests that verify abnormal cases by string matching need expected value updates (baserCMS track record:UploaderCategoriesControllerTestAdmin/Api eachtest_copy)。
⚠️ Chained errors during a full test run should first be triaged by rerunning: in this codebase, during a full-run, transient contamination occasionally occurs where "a preceding test pollutes global state (plugin configuration Configure etc.) → afterwards almost all tests chain-error with Application bootstrap
PluginCollection::create(): $config ... null givenetc.". If the plugin's standalone suite (e.g.plugins/baser-core/tests/TestCase) passes and a rerun is clean, thenit is not code-caused but flaky. Always confirm with isolation and reruns.
How to proceed with investigation (checklist)
php -l(syntax check) andlogs/debug.logでFile and line where the deprecation warning occursidentify.- Classify and fix which pattern in the relevant version section applies. Items that can be mechanically replaced (such as non-canonical casts) are batch-detected with grep.
php -lRecheck with (whether the warning has disappeared).- Check the relevant test individually with
--filter→ finally rerun all tests.
Related: Framework-caused issues (event return values, Association duplication, ResultSet, FormProtection, etc.) are cakephp-migration to skills.