Skip to content

Commit 6cdc329

Browse files
committed
Add PHP compatibility layer for versions 7.0 through 8.4+
New file: incs/compat.inc.php — loaded FIRST in the bootstrap chain. Polyfills for removed functions: - utf8_encode/utf8_decode (removed in PHP 8.2) - each() (removed in PHP 8.0) - create_function() (removed in PHP 8.0) - FILTER_SANITIZE_STRING constant (removed in PHP 8.1) - str_contains/str_starts_with/str_ends_with (added in PHP 8.0) Deprecation suppression for: - strftime() (deprecated PHP 8.1) - Passing null to non-nullable parameters (PHP 8.0+) - Implicitly nullable parameters (PHP 8.4) - ${var} string interpolation (PHP 8.2) Injected into 3 bootstrap entry points: - incs/functions.inc.php (main pages) - incs/functions_nm.inc.php (no-MySQL pages) - install.php (installer) This should fix 500 Internal Server Errors for users on PHP 8.2+ who were hitting utf8_encode removal and deprecated function crashes. Tested on PHP 8.2.4 — all polyfills verified.
1 parent 6244a38 commit 6cdc329

4 files changed

Lines changed: 239 additions & 2 deletions

File tree

incs/compat.inc.php

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php
2+
/**
3+
* TicketsCAD PHP Compatibility Layer
4+
*
5+
* Polyfills functions removed or deprecated across PHP versions.
6+
* Include this FIRST — before any other code runs.
7+
*
8+
* Supports: PHP 7.0 through 8.4+
9+
*
10+
* Changes by PHP version:
11+
* PHP 8.4: Implicit nullable parameters deprecated
12+
* PHP 8.2: utf8_encode/utf8_decode REMOVED, ${var} interpolation deprecated
13+
* PHP 8.1: strftime() deprecated, FILTER_SANITIZE_STRING removed
14+
* PHP 8.0: each() REMOVED, create_function() REMOVED, stricter type juggling
15+
* PHP 7.4: array_key_exists() on objects deprecated
16+
* PHP 7.2: each() deprecated, create_function() deprecated
17+
*
18+
* @package TicketsCAD
19+
* @since v3.44.2
20+
*/
21+
22+
// ═══════════════════════════════════════════════════════════════
23+
// PHP 8.2+ — utf8_encode / utf8_decode REMOVED
24+
// These were thin wrappers for ISO-8859-1 ↔ UTF-8 conversion.
25+
// Polyfill using mb_convert_encoding or manual byte conversion.
26+
// ═══════════════════════════════════════════════════════════════
27+
28+
if (!function_exists('utf8_encode')) {
29+
function utf8_encode($string) {
30+
if (function_exists('mb_convert_encoding')) {
31+
return mb_convert_encoding($string, 'UTF-8', 'ISO-8859-1');
32+
}
33+
// Manual fallback: convert each byte > 0x7F to UTF-8 two-byte sequence
34+
$output = '';
35+
$len = strlen($string);
36+
for ($i = 0; $i < $len; $i++) {
37+
$byte = ord($string[$i]);
38+
if ($byte < 0x80) {
39+
$output .= $string[$i];
40+
} else {
41+
$output .= chr(0xC0 | ($byte >> 6)) . chr(0x80 | ($byte & 0x3F));
42+
}
43+
}
44+
return $output;
45+
}
46+
}
47+
48+
if (!function_exists('utf8_decode')) {
49+
function utf8_decode($string) {
50+
if (function_exists('mb_convert_encoding')) {
51+
return mb_convert_encoding($string, 'ISO-8859-1', 'UTF-8');
52+
}
53+
// Manual fallback
54+
$output = '';
55+
$len = strlen($string);
56+
for ($i = 0; $i < $len; $i++) {
57+
$byte = ord($string[$i]);
58+
if ($byte < 0x80) {
59+
$output .= $string[$i];
60+
} elseif (($byte & 0xE0) === 0xC0) {
61+
$next = ($i + 1 < $len) ? ord($string[++$i]) : 0;
62+
$codepoint = (($byte & 0x1F) << 6) | ($next & 0x3F);
63+
$output .= ($codepoint < 256) ? chr($codepoint) : '?';
64+
} else {
65+
// 3-byte or 4-byte sequences map to '?' in ISO-8859-1
66+
if (($byte & 0xF0) === 0xE0) { $i += 2; }
67+
elseif (($byte & 0xF8) === 0xF0) { $i += 3; }
68+
$output .= '?';
69+
}
70+
}
71+
return $output;
72+
}
73+
}
74+
75+
// ═══════════════════════════════════════════════════════════════
76+
// PHP 8.1+ — FILTER_SANITIZE_STRING removed
77+
// ═══════════════════════════════════════════════════════════════
78+
79+
if (!defined('FILTER_SANITIZE_STRING')) {
80+
define('FILTER_SANITIZE_STRING', FILTER_DEFAULT);
81+
}
82+
83+
// ═══════════════════════════════════════════════════════════════
84+
// PHP 8.0+ — each() REMOVED
85+
// Used by very old code to iterate arrays. Replace with foreach
86+
// where possible, but polyfill for code we can't easily change.
87+
// ═══════════════════════════════════════════════════════════════
88+
89+
if (!function_exists('each')) {
90+
function each(&$array) {
91+
$value = current($array);
92+
$key = key($array);
93+
if ($key === null) {
94+
return false;
95+
}
96+
next($array);
97+
return array(1 => $value, 'value' => $value, 0 => $key, 'key' => $key);
98+
}
99+
}
100+
101+
// ═══════════════════════════════════════════════════════════════
102+
// PHP 8.0+ — create_function() REMOVED
103+
// Polyfill wraps eval() — not ideal but maintains BC.
104+
// New code should never use this.
105+
// ═══════════════════════════════════════════════════════════════
106+
107+
if (!function_exists('create_function')) {
108+
function create_function($args, $code) {
109+
static $counter = 0;
110+
$funcName = '__compat_lambda_' . (++$counter);
111+
eval("function {$funcName}({$args}) { {$code} }");
112+
return $funcName;
113+
}
114+
}
115+
116+
// ═══════════════════════════════════════════════════════════════
117+
// PHP 8.1+ — strftime() DEPRECATED (E_DEPRECATED warnings)
118+
// Many legacy date formatting calls use strftime. Suppress the
119+
// deprecation warning since the function still works through 8.x.
120+
// ═══════════════════════════════════════════════════════════════
121+
122+
// We don't polyfill strftime() because it still works in PHP 8.x,
123+
// just throws E_DEPRECATED. The error_reporting below handles it.
124+
// A full replacement would require IntlDateFormatter which may not
125+
// be available on all systems.
126+
127+
// ═══════════════════════════════════════════════════════════════
128+
// PHP 7.2+ — Suppress deprecation notices from legacy code
129+
// This prevents E_DEPRECATED from breaking JSON output or causing
130+
// 500 errors when the error handler is strict.
131+
// ═══════════════════════════════════════════════════════════════
132+
133+
// Only suppress deprecation notices — all real errors still reported
134+
$currentLevel = error_reporting();
135+
if ($currentLevel & E_DEPRECATED) {
136+
error_reporting($currentLevel & ~E_DEPRECATED);
137+
}
138+
139+
// ═══════════════════════════════════════════════════════════════
140+
// PHP 8.0+ — str_contains / str_starts_with / str_ends_with
141+
// These are useful new functions. Polyfill for PHP < 8.0.
142+
// ═══════════════════════════════════════════════════════════════
143+
144+
if (!function_exists('str_contains')) {
145+
function str_contains(string $haystack, string $needle): bool {
146+
return $needle === '' || strpos($haystack, $needle) !== false;
147+
}
148+
}
149+
150+
if (!function_exists('str_starts_with')) {
151+
function str_starts_with(string $haystack, string $needle): bool {
152+
return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
153+
}
154+
}
155+
156+
if (!function_exists('str_ends_with')) {
157+
function str_ends_with(string $haystack, string $needle): bool {
158+
return $needle === '' || substr($haystack, -strlen($needle)) === $needle;
159+
}
160+
}
161+
162+
// ═══════════════════════════════════════════════════════════════
163+
// PHP 8.0+ — Stricter error handling for null parameters
164+
// Many legacy functions pass null where string is expected.
165+
// Set up a custom error handler to silently convert these.
166+
// ═══════════════════════════════════════════════════════════════
167+
168+
if (PHP_MAJOR_VERSION >= 8) {
169+
$previousHandler = set_error_handler(function ($severity, $message, $file, $line) use (&$previousHandler) {
170+
// Suppress "passing null to non-nullable parameter" deprecation
171+
if ($severity === E_DEPRECATED && strpos($message, 'Passing null to parameter') !== false) {
172+
return true; // Suppress
173+
}
174+
// Suppress "strftime() is deprecated" (PHP 8.1+)
175+
if ($severity === E_DEPRECATED && strpos($message, 'strftime()') !== false) {
176+
return true; // Suppress
177+
}
178+
// Suppress "implicitly nullable parameter" (PHP 8.4+)
179+
if ($severity === E_DEPRECATED && strpos($message, 'implicitly nullable') !== false) {
180+
return true; // Suppress
181+
}
182+
// Suppress "Use of ${var} in strings is deprecated" (PHP 8.2+)
183+
if ($severity === E_DEPRECATED && strpos($message, '${') !== false) {
184+
return true; // Suppress
185+
}
186+
// Pass everything else to the previous handler or PHP's default
187+
if ($previousHandler) {
188+
return call_user_func($previousHandler, $severity, $message, $file, $line);
189+
}
190+
return false; // Let PHP handle it
191+
});
192+
}
193+
194+
// ═══════════════════════════════════════════════════════════════
195+
// PHP 7.0+ — Null coalescing for array access safety
196+
// Helper function for legacy code that uses array_key_exists on
197+
// potentially null arrays.
198+
// ═══════════════════════════════════════════════════════════════
199+
200+
if (!function_exists('array_get')) {
201+
/**
202+
* Safely get a value from an array with a default.
203+
* Works with null arrays and missing keys.
204+
*/
205+
function array_get($array, $key, $default = null) {
206+
if (!is_array($array)) return $default;
207+
return array_key_exists($key, $array) ? $array[$key] : $default;
208+
}
209+
}
210+
211+
// ═══════════════════════════════════════════════════════════════
212+
// PHP 8.0+ — mysqli_result changes
213+
// fetch_assoc() returns null instead of false on empty result.
214+
// This doesn't break while() loops but may break strict === false
215+
// checks. No polyfill needed, but document for awareness.
216+
// ═══════════════════════════════════════════════════════════════
217+
218+
// No polyfill needed — while($row = $result->fetch_assoc()) works
219+
// the same with both null and false as the loop terminator.
220+
221+
// ═══════════════════════════════════════════════════════════════
222+
// Startup diagnostic — log PHP version on first load
223+
// ═══════════════════════════════════════════════════════════════
224+
225+
// Log once per session to help with debugging
226+
if (function_exists('session_status') && session_status() === PHP_SESSION_NONE) {
227+
// Don't start session here — let the caller do it
228+
} elseif (isset($_SESSION) && !isset($_SESSION['_compat_logged'])) {
229+
$_SESSION['_compat_logged'] = true;
230+
// Optionally log: error_log("TicketsCAD: PHP " . PHP_VERSION . " compat layer loaded");
231+
}

incs/functions.inc.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<?php
2+
// Load PHP version compatibility layer FIRST — polyfills removed functions
3+
// for PHP 7.0 through 8.4+ (utf8_encode, each, create_function, etc.)
4+
require_once(__DIR__ . '/compat.inc.php');
5+
26
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
37

48
// Suppress display of errors in AJAX endpoints to prevent PHP warnings from

incs/functions_nm.inc.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@
138138
7/10/13 Revisions to function show_actions( to correct failure to show patients if no actions.
139139
9/10/13 Added function show_unit_log()
140140
*/
141-
error_reporting(E_ALL);
141+
require_once(__DIR__ . '/compat.inc.php');
142+
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
142143

143144
// { -- dummy
144145
//

install.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* - Supports clean install, upgrade sync, and write-config modes.
66
* - Streams step-by-step progress and records installed _version for parity checks.
77
*/
8-
error_reporting(E_ALL);
8+
require_once(__DIR__ . '/incs/compat.inc.php');
9+
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
910
if (function_exists('mysqli_report')) { mysqli_report(MYSQLI_REPORT_OFF); }
1011

1112
require_once __DIR__ . '/incs/versions.inc.php';

0 commit comments

Comments
 (0)