Code analysis for improvements, refactoring, and modernization opportunities.
str_starts_with(), str_ends_with(), Throwablepad prefix convention throughout$GLOBALS and global declarationsinclude statements as control flow mechanism$padVariable[$pad] patternAlready using modern features:
str_starts_with(), str_ends_with(), str_contains()Throwable type for exception handlingReadonly Properties (PHP 8.1)
// Current: globals
global $padPage, $padDir;
// Modern: readonly class properties
readonly class PadContext {
public function __construct(
public string $page,
public string $dir,
public int $level,
) {}
}
Enums (PHP 8.1) for error levels, output types:
// Current
$padErrorLevel = 'all'; // 'none', 'error', 'warning', 'notice', 'all'
$padOutputType = 'web'; // 'web', 'file', 'download', 'console'
// Modern
enum ErrorLevel: string {
case None = 'none';
case Error = 'error';
case Warning = 'warning';
case Notice = 'notice';
case All = 'all';
}
enum OutputType: string {
case Web = 'web';
case File = 'file';
case Download = 'download';
case Console = 'console';
}
Fibers (PHP 8.1) for async operations:
// Potential for async template rendering or parallel data fetching
Constructor Property Promotion (PHP 8.0):
// For any future classes
class DatabaseConnection {
public function __construct(
private string $host,
private string $user,
private string $password,
private string $database,
private ?mysqli $connection = null,
) {}
}
// Typical function
function padFieldValue ($name) {
global $pad, $padCurrent, $padOccur, $padTag, $padData, $padFields;
// ... uses globals throughout
}
global $pad declarations across 58 filesOption A: Context Object (Minimal Change)
// Create a context class
class PadContext {
public int $level = -1;
public array $current = [];
public array $occur = [];
public array $tag = [];
public array $data = [];
// ... other level-indexed arrays
private static ?PadContext $instance = null;
public static function get(): PadContext {
return self::$instance ??= new PadContext();
}
}
// Usage - gradually migrate
function padFieldValue($name, ?PadContext $ctx = null) {
$ctx = $ctx ?? PadContext::get();
$pad = $ctx->level;
// ... use $ctx->current[$pad] instead of $padCurrent[$pad]
}
Option B: Dependency Injection (Full Modernization)
class LevelProcessor {
public function __construct(
private PadContext $context,
private TypeResolver $typeResolver,
private ExpressionEvaluator $evaluator,
) {}
public function process(string $content): string {
// All dependencies explicit
}
}
Migration Path:
PadContext class with static singleton$ctx parameter to new functions// level/level.php
if ( $padRestart )
return include PAD . 'level/restart.php';
if ( file_exists ( PAD . "types/$padType.php" ) )
include PAD . "types/$padType.php";
Strategy Pattern with Autoloading
// Type handlers as classes
namespace Pad\Types;
interface TypeHandler {
public function handle(PadContext $ctx): mixed;
}
class AppType implements TypeHandler {
public function handle(PadContext $ctx): mixed {
// Logic from types/app.php
}
}
class DataType implements TypeHandler {
public function handle(PadContext $ctx): mixed {
// Logic from types/data.php
}
}
// Registry
class TypeRegistry {
private array $handlers = [];
public function register(string $type, TypeHandler $handler): void {
$this->handlers[$type] = $handler;
}
public function get(string $type): ?TypeHandler {
return $this->handlers[$type] ?? null;
}
}
Composer Autoloading
{
"autoload": {
"psr-4": {
"Pad\\": "src/"
},
"files": [
"src/functions.php"
]
}
}
Migration Path:
composer.json with PSR-4 autoloadinginclude fallback for backward compatibility// lib/db.php
function db($sql, $vars = []) {
global $padSqlConnect, $padSqlHost, $padSqlUser, $padSqlPassword, $padSqlDatabase;
if (!isset($padSqlConnect))
$padSqlConnect = padDbConnect($padSqlHost, $padSqlUser, $padSqlPassword, $padSqlDatabase);
return padDbPart2($padSqlConnect, $sql, $vars);
}
// Custom placeholder replacement
$sql = str_replace('{0}', mysqli_real_escape_string($conn, $replace), $sql);
class Database {
private static ?PDO $connection = null;
public static function connection(): PDO {
if (self::$connection === null) {
$config = Config::get();
self::$connection = new PDO(
"mysql:host={$config->dbHost};dbname={$config->dbName};charset=utf8mb4",
$config->dbUser,
$config->dbPassword,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
}
return self::$connection;
}
public static function query(string $sql, array $params = []): array {
$stmt = self::connection()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public static function execute(string $sql, array $params = []): int {
$stmt = self::connection()->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
public static function insertId(): string {
return self::connection()->lastInsertId();
}
}
// Usage
$users = Database::query(
"SELECT * FROM users WHERE status = ? AND created > ?",
['active', '2024-01-01']
);
Query Builder Option
class QueryBuilder {
private string $table;
private array $wheres = [];
private array $params = [];
public static function table(string $table): self {
$builder = new self();
$builder->table = $table;
return $builder;
}
public function where(string $column, string $operator, mixed $value): self {
$this->wheres[] = "$column $operator ?";
$this->params[] = $value;
return $this;
}
public function get(): array {
$sql = "SELECT * FROM {$this->table}";
if ($this->wheres) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
return Database::query($sql, $this->params);
}
}
// Usage
$users = QueryBuilder::table('users')
->where('status', '=', 'active')
->where('age', '>', 18)
->get();
Migration Path:
{0} placeholders to ?db() function in favor of new class// config/config.php
$padErrorAction = 'pad';
$padErrorLevel = 'all';
$padSqlHost = '127.0.0.1';
// ... all as global variables
class Config {
private static ?Config $instance = null;
private array $values = [];
private function __construct() {
$this->loadDefaults();
$this->loadEnvironment();
$this->loadFile();
}
public static function get(?string $key = null): mixed {
$instance = self::$instance ??= new Config();
if ($key === null) {
return $instance;
}
return $instance->values[$key] ?? null;
}
private function loadDefaults(): void {
$this->values = [
'error.action' => 'pad',
'error.level' => 'all',
'error.log' => true,
'db.host' => '127.0.0.1',
'db.name' => 'app',
'db.user' => 'app',
'db.password' => '',
'output.type' => 'web',
'cache.enabled' => false,
];
}
private function loadEnvironment(): void {
// Load from .env file or environment variables
if ($host = getenv('PAD_DB_HOST')) {
$this->values['db.host'] = $host;
}
// ... etc
}
private function loadFile(): void {
$file = APP . '_config.php';
if (file_exists($file)) {
$config = include $file;
$this->values = array_merge($this->values, $config);
}
}
public function __get(string $name): mixed {
$key = str_replace('_', '.', $name);
return $this->values[$key] ?? null;
}
}
// Usage
$host = Config::get('db.host');
// or
$config = Config::get();
$host = $config->db_host;
The framework already has sophisticated error handling. Enhancements:
interface LoggerInterface {
public function emergency(string $message, array $context = []): void;
public function alert(string $message, array $context = []): void;
public function critical(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
public function warning(string $message, array $context = []): void;
public function notice(string $message, array $context = []): void;
public function info(string $message, array $context = []): void;
public function debug(string $message, array $context = []): void;
}
class PadLogger implements LoggerInterface {
public function error(string $message, array $context = []): void {
$formatted = $this->interpolate($message, $context);
if (Config::get('error.log')) {
error_log($formatted);
}
if (Config::get('error.report')) {
$this->dumpToFile($formatted, $context);
}
}
private function interpolate(string $message, array $context): string {
$replace = [];
foreach ($context as $key => $val) {
$replace['{' . $key . '}'] = $val;
}
return strtr($message, $replace);
}
}
// Usage
$logger = new PadLogger();
$logger->error('SQL error: {error} in query: {query}', [
'error' => $mysqli_error,
'query' => $sql,
]);
namespace Pad\Exceptions;
class PadException extends \Exception {}
class TemplateException extends PadException {}
class DatabaseException extends PadException {}
class ConfigurationException extends PadException {}
class ValidationException extends PadException {}
// Usage
throw new DatabaseException("Connection failed: $error", previous: $e);
No testing infrastructure exists.
Directory Structure
pad/
├── src/ # Future: refactored code
├── tests/
│ ├── Unit/
│ │ ├── EvalTest.php
│ │ ├── FunctionsTest.php
│ │ └── ValidationTest.php
│ ├── Integration/
│ │ ├── TemplateTest.php
│ │ └── DatabaseTest.php
│ └── bootstrap.php
├── phpunit.xml
└── composer.json
phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
</phpunit>
Example Tests
// tests/Unit/FunctionsTest.php
class FunctionsTest extends TestCase {
public function testTrimFunction(): void {
$value = ' hello ';
$result = include PAD . 'functions/trim.php';
$this->assertEquals('hello', $result);
}
public function testUpperFunction(): void {
$value = 'hello';
$result = include PAD . 'functions/upper.php';
$this->assertEquals('HELLO', $result);
}
/**
* @dataProvider dateFormatProvider
*/
public function testDateFunction(int $timestamp, string $format, string $expected): void {
$value = $timestamp;
$parm = [$format];
$result = include PAD . 'functions/date.php';
$this->assertEquals($expected, $result);
}
public static function dateFormatProvider(): array {
return [
[1702483200, 'Y-m-d', '2023-12-13'],
[1702483200, 'H:i', '12:00'],
];
}
}
// tests/Unit/ValidationTest.php
class ValidationTest extends TestCase {
public function testValidName(): void {
$this->assertTrue(padValid('userName'));
$this->assertTrue(padValid('user_name'));
$this->assertFalse(padValid(''));
$this->assertFalse(padValid('123name'));
}
public function testValidFile(): void {
define('APP', '/app/');
define('DAT', '/data/');
define('PAD', '/pad/');
$this->assertTrue(padValidFile('/app/test.php'));
$this->assertFalse(padValidFile('/etc/passwd'));
$this->assertFalse(padValidFile('../../../etc/passwd'));
}
}
Current: Multiple file_exists() checks per request
if (file_exists(PAD . "types/$type.php"))
include PAD . "types/$type.php";
Recommended: Precomputed registry
class FileRegistry {
private static array $cache = [];
public static function exists(string $path): bool {
if (!isset(self::$cache[$path])) {
self::$cache[$path] = file_exists($path);
}
return self::$cache[$path];
}
public static function warmup(): void {
// Pre-scan directories on first request
self::scanDirectory(PAD . 'types/');
self::scanDirectory(PAD . 'tags/');
self::scanDirectory(PAD . 'functions/');
}
}
// Ensure opcache is configured for production
// php.ini recommendations:
// opcache.enable=1
// opcache.memory_consumption=256
// opcache.interned_strings_buffer=16
// opcache.max_accelerated_files=10000
// opcache.validate_timestamps=0 (production only)
class TemplateCache {
public static function get(string $template): ?string {
$cacheFile = DAT . 'cache/' . md5($template) . '.php';
if (file_exists($cacheFile)) {
$cached = include $cacheFile;
if ($cached['mtime'] >= filemtime(APP . $template)) {
return $cached['content'];
}
}
return null;
}
public static function set(string $template, string $compiled): void {
$cacheFile = DAT . 'cache/' . md5($template) . '.php';
$content = [
'mtime' => time(),
'content' => $compiled,
];
file_put_contents($cacheFile, '<?php return ' . var_export($content, true) . ';');
}
}
Current: Custom escaping with potential for bypass
$sql = str_replace('{0}', mysqli_real_escape_string($conn, $replace), $sql);
Recommended: Prepared statements only (see Database section)
class Validator {
public static function string(mixed $value, int $maxLength = 255): string {
if (!is_string($value)) {
throw new ValidationException('Expected string');
}
return substr($value, 0, $maxLength);
}
public static function int(mixed $value, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX): int {
$value = filter_var($value, FILTER_VALIDATE_INT);
if ($value === false || $value < $min || $value > $max) {
throw new ValidationException("Expected integer between $min and $max");
}
return $value;
}
public static function email(mixed $value): string {
$value = filter_var($value, FILTER_VALIDATE_EMAIL);
if ($value === false) {
throw new ValidationException('Invalid email');
}
return $value;
}
}
class CSRF {
public static function token(): string {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function verify(string $token): bool {
return hash_equals($_SESSION['csrf_token'] ?? '', $token);
}
public static function field(): string {
return '<input type="hidden" name="_csrf" value="' . self::token() . '">';
}
}
composer.json
{
"require-dev": {
"phpunit/phpunit": "^10.0",
"phpstan/phpstan": "^1.10",
"squizlabs/php_codesniffer": "^3.7",
"friendsofphp/php-cs-fixer": "^3.0"
},
"scripts": {
"test": "phpunit",
"analyse": "phpstan analyse src tests",
"cs-check": "phpcs",
"cs-fix": "php-cs-fixer fix"
}
}
phpstan.neon
parameters:
level: 6
paths:
- src
- lib
excludePaths:
- tests
composer.json with autoloadingConfig class (backward compatible)PadContext singleton for gradual global reduction| Area | Current | Recommended | Priority |
|---|---|---|---|
| PHP Version | 8.0 | 8.1+ | Low |
| Global State | Heavy | Context object | High |
| Autoloading | None | PSR-4 | High |
| Database | mysqli + custom escape | PDO + prepared | High |
| Testing | None | PHPUnit | High |
| Configuration | Globals | Config class | Medium |
| Error Handling | Good | PSR-3 logging | Medium |
| Type System | Includes | Strategy pattern | Low |
| Code Quality | None | PHPStan + PHPCS | Medium |
The framework is functional and battle-tested. Modernization should be incremental, maintaining backward compatibility while gradually improving code quality, testability, and security.