An old 1C-Bitrix project moved to PHP 8 usually breaks in custom code, not in the core: custom modules, init.php, event handlers. PHP 8 stopped quietly accepting null where an array or a number is expected, so code that "somehow worked" for years starts throwing TypeError and returning HTTP 500. The fix is surgical: find each spot in the log, add a type check, and bring the database schema back in line with the platform's own system check.
What happened
The project moved to a new server with PHP 8, and several things broke at once:
- the admin panel returned a 500 error: under PHP 8's strict typing,
array_key_existsreceivednullinstead of an array; - the built-in system check reported wrong field types in database tables;
- drag-and-drop of homepage blocks froze, with a 404 from the Push and Pull module and a Sortable.js crash;
- the background import failed with
TypeErrorbecause a string was passed togmdate.
Together this blocked site management and catalog updates. The site opened, but nobody could work with it.
Why legacy Bitrix breaks on PHP 8
- Internal functions got strict.
array_key_exists($key, null)used to emit a warning and returnfalse. In PHP 8 it is aTypeError, a fatal error. The same goes forcount()on a non-array, arithmetic on non-numeric strings, andgmdate()given a string. mbstring.func_overloadis gone. Very old Bitrix cores relied on string function overloading, which PHP 8 removed entirely. Update the core to a PHP 8-compatible version first, or fixing your own code is pointless.- Custom code never got updated. The core and Marketplace modules receive updates;
init.php, custom components and patches to third-party modules do not. That is where most of the breakage lives.
Step 1. See the real error
A bare 500 tells you nothing. Temporarily enable detailed exceptions in /bitrix/.settings.php, preferably on a copy of the site:
'exception_handling' => [
'value' => [
'debug' => true, // TODO: set back to false after debugging
'log' => [
'settings' => [
// TODO: the directory must exist and be closed to the web
'file' => 'local/logs/php_errors.log',
'log_size' => 1000000,
],
],
// keep the other keys of this section as they were
],
'readonly' => false,
],Now you get a file and a line instead of a blank 500. Also read the server's PHP log: errors that happen before the core loads only show up there.
Step 2. The admin 500: check the type before the call
The admin crash came from lib/traits/admin.php, where the function received null instead of an options array. I added an is_array check:
if (is_array($arOption) && array_key_exists($key, $arOption)) {
// Execute logic
}It changes nothing when the data is valid and stops the crash when it is not. That is the general recipe for most TypeErrors in legacy code: guard each spot where the wrong type can arrive instead of rewriting the module.
Step 3. The database schema
The system check (Settings → Tools → System check) compares tables against the reference schema. Here some fields had types the platform did not expect. I fixed them with direct SQL:
ALTER TABLE b_intranet_invitation_link CHANGE CREATED_BY CREATED_BY int NULL DEFAULT NULL;
ALTER TABLE b_booking_wait_list_item CHANGE CREATED_BY CREATED_BY bigint NOT NULL;Back up before any ALTER TABLE on production, and try the built-in fix in the system check first. Direct SQL is for what the standard tool cannot repair.
Step 4. Frozen drag-and-drop
Block reordering failed for two reasons: a 404 from Push and Pull and a script crash in options.php, which called replace on undefined because a checkbox had no name attribute. A null check fixed it:
var inputName = $(this).find('input[type="checkbox"]').attr('name');
if (inputName) {
order.push(inputName.replace(current_type+' *', '').replace('* '+current_site, ''));
}One JS error stops every script after it on the page, so "drag-and-drop is broken" may mean something else failed earlier. Start with the browser console.
Step 5. TypeError in the background import
The import failed in the CMyLastModified::apply handler from init.php: gmdate received a date string, and PHP 8 requires an integer. I changed the handler to convert text dates to a timestamp first. The shape of the fix:
// TODO: adapt to your own handler
$timestamp = is_numeric($value) ? (int) $value : strtotime((string) $value);
if ($timestamp !== false) {
$header = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
}Errors like this only surface in the background, in agents, cron and imports, so test those separately.
Result
- 100% admin panel stability
- 0 type errors in the background import
- 99.9% service uptime after migration
- Database schema errors fully resolved
Moving an old Bitrix site to PHP 8: the order that saves time
- Copy the site and back up the database, migrate the copy first.
- Update the core and modules to PHP 8-compatible versions.
- Enable exception logging and walk through the key flows: public pages, admin, cart and checkout, forms.
- Run background jobs separately (agents, cron, the 1C import) and read the log.
- Run the system check and fix schema mismatches.
- Search your own code for risky spots:
array_key_exists,count,implode, date handling, arithmetic on request and option values. - Only then switch PHP on production, with a rollback plan.
FAQ
Why does the Bitrix admin return a 500 error after moving to PHP 8?
Usually custom code passes null or a string where PHP 8 strictly requires an array or a number. Enable exception logging in .settings.php, find the file and line, and add a type check.
Does old Bitrix run on PHP 8?
Only after updating the core to a PHP 8-compatible version. Very old versions depend on mbstring.func_overload, which PHP 8 removed.
How do I fix wrong field types in the Bitrix database?
Run the system check and use its built-in fix. If that fails, alter the columns to match the reference schema with ALTER TABLE, after a backup.