├── .gitignore ├── App ├── Cache │ ├── CacheInterface.php │ ├── file.php │ └── redis.php ├── Config.php ├── Controller │ ├── Admin │ │ └── User.php │ └── Home.php ├── DB │ ├── CommandBuilderInterface.php │ ├── Db.php │ ├── MysqlCommandBuilder.php │ ├── ORM │ │ ├── Builder.php │ │ ├── Model.php │ │ └── Relations │ │ │ ├── BelongsToManyRelation.php │ │ │ ├── BelongsToRelation.php │ │ │ ├── HasManyRelation.php │ │ │ ├── HasOneRelation.php │ │ │ └── Relation.php │ └── pdo.php ├── Hook │ ├── AfterControllerHook.php │ ├── BeforeControllerHook.php │ └── CustomControllerHook.php ├── Model │ ├── Post.php │ ├── Profile.php │ └── User.php ├── Routes.php ├── Service │ └── TestService.php ├── Session │ ├── SessionAdapterInterface.php │ ├── db.php │ └── redis.php └── View │ └── default │ ├── 404.html │ ├── 500.html │ ├── Home │ └── index.html │ └── base.html ├── CHANGELOG.md ├── Cache └── .gitkeep ├── Config └── default.php ├── Core ├── BaseController.php ├── Error.php ├── Model.php ├── Router.php └── View.php ├── Engine ├── Interface │ └── ServiceProviderInterface.php ├── agent.php ├── cache.php ├── config.php ├── db.php ├── di.php ├── file.php ├── hook.php ├── language.php ├── log.php ├── profile.php ├── request.php ├── response.php ├── session.php └── theme.php ├── README.md ├── TODO.md ├── composer.json ├── composer.lock ├── log └── .gitkeep ├── phpstan.neon ├── phpstan.neon.list └── public ├── .htaccess └── index.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Ignore composer vendor directory 4 | /vendor 5 | 6 | # Ignore PHPStorm project files 7 | /.idea 8 | 9 | # Ignore .env files used for environment configuration 10 | /.env 11 | 12 | # Ignore log files 13 | /log/* 14 | 15 | # Ignore cache files 16 | /Cache/* 17 | 18 | /mindmap/* 19 | 20 | /phpmvc.mindnode/* 21 | 22 | /test/* -------------------------------------------------------------------------------- /App/Cache/CacheInterface.php: -------------------------------------------------------------------------------- 1 | expire = $expire; 23 | } 24 | 25 | /** 26 | * Retrieves cached data based on the specified key. 27 | * 28 | * @param string $key The cache key. 29 | * @return array The cached data associated with the key. 30 | */ 31 | public function get(string $key): array 32 | { 33 | $files = glob($this->storage . "cache.$key.*"); 34 | 35 | if ($files) { 36 | return json_decode(file_get_contents($files[0]), true); 37 | } 38 | return []; 39 | } 40 | 41 | /** 42 | * Stores data in the cache with the specified key and optional expiration time. 43 | * 44 | * @param string $key The cache key. 45 | * @param mixed $data The data to be stored in the cache. 46 | * @param int|null $expire The expiration time for the cached data in seconds. If null, the default expiration time should be used. 47 | */ 48 | public function set(string $key, mixed $data, int $expire = null): void 49 | { 50 | $this->delete($key); 51 | $expire = $expire ?? $this->expire; 52 | $time = time() + $expire; 53 | file_put_contents($this->storage . "cache.$key.$time", json_encode($data)); 54 | } 55 | 56 | /** 57 | * Deletes cached data based on the specified key. 58 | * 59 | * @param string $key The cache key. 60 | */ 61 | public function delete(string $key): void 62 | { 63 | $files = glob($this->storage . "cache.$key.*"); 64 | 65 | if ($files) { 66 | foreach ($files as $file) { 67 | if (!@unlink($file)) { 68 | clearstatcache(false, $file); 69 | } 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Destructor to automatically clean up expired cache files. 76 | */ 77 | public function __destruct() 78 | { 79 | $files = glob($this->storage . 'cache.*'); 80 | 81 | if ($files && mt_rand(1, 100) == 1) { 82 | foreach ($files as $file) { 83 | $time = substr(strrchr($file, '.'), 1); 84 | if (time() > $time) { 85 | if (!@unlink($file)) { 86 | clearstatcache(false, $file); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /App/Cache/redis.php: -------------------------------------------------------------------------------- 1 | expire = $expire; 22 | $this->redis = new \Redis(); 23 | $this->redis->connect(\App\Config::cache_hostname, \App\Config::cache_port); 24 | 25 | // Check if password is required 26 | $requiresAuth = $this->redis->config('GET', 'requirepass'); 27 | if (isset($requiresAuth['requirepass']) && !empty($requireAuth['requirepass'])) { 28 | // Password is required, authenticate 29 | $this->redis->auth(\App\Config::cache_password); 30 | } 31 | } 32 | 33 | /** 34 | * Stores data in the cache with the specified key and optional expiration time. 35 | * 36 | * @param string $key The cache key. 37 | * @param mixed $data The data to be stored in the cache. 38 | * @param int|null $expire The expiration time for the cached data in seconds. If null, the default expiration time should be used. 39 | */ 40 | public function set(string $key, mixed $data, int $expire = null): void 41 | { 42 | $expire = $expire ?? $this->expire; 43 | $res = $this->redis->set($key, json_encode($data)); 44 | if ($res) { 45 | $this->redis->expire($key, $expire); 46 | } 47 | } 48 | 49 | /** 50 | * Retrieves cached data based on the specified key. 51 | * 52 | * @param string $key The cache key. 53 | * @return array The cached data associated with the key. 54 | */ 55 | public function get(string $key): array 56 | { 57 | $data = $this->redis->get($key); 58 | return $data ? json_decode($data, true) : []; 59 | } 60 | 61 | /** 62 | * Deletes cached data based on the specified key. 63 | * 64 | * @param string $key The cache key. 65 | */ 66 | public function delete(string $key): void 67 | { 68 | $this->redis->del($key); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /App/Config.php: -------------------------------------------------------------------------------- 1 | $name]); 16 | } 17 | 18 | public function updateAction($params) 19 | { 20 | var_dump($this->session->adapter->read($this->session->getId())); 21 | return $params['id']; 22 | } 23 | 24 | public function __destruct() 25 | { 26 | echo '123'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /App/DB/CommandBuilderInterface.php: -------------------------------------------------------------------------------- 1 | table = $table; 92 | return $this; 93 | } 94 | 95 | /** 96 | * Set the columns to select. 97 | * 98 | * @param string|array $select The columns to select. 99 | * @return CommandBuilderInterface 100 | */ 101 | public function select(string|array $select): CommandBuilderInterface 102 | { 103 | if (is_string($select)) { 104 | if (!in_array($select, $this->select)) { 105 | array_push($this->select, $select); 106 | } 107 | } else if (is_array($select)) { 108 | $this->select = $select; 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Set the ORDER BY clause. 116 | * 117 | * @param string $col The column to order by. 118 | * @param string $direction The sort direction. 119 | * @return CommandBuilderInterface 120 | */ 121 | public function orderBy(string $col, string $direction = "ASC"): CommandBuilderInterface 122 | { 123 | $this->orderBy = "ORDER BY " . $col . " " . $direction; 124 | return $this; 125 | } 126 | 127 | /** 128 | * Set the LIMIT clause. 129 | * 130 | * @param int $limit The number of rows to limit. 131 | * @return CommandBuilderInterface 132 | */ 133 | public function limit(int $limit): CommandBuilderInterface 134 | { 135 | $this->limit = 'LIMIT ' . $limit; 136 | return $this; 137 | } 138 | 139 | /** 140 | * Set the OFFSET clause. 141 | * 142 | * @param int $offset The number of rows to offset. 143 | * @return CommandBuilderInterface 144 | */ 145 | public function offset(int $offset): CommandBuilderInterface 146 | { 147 | $this->offset = 'OFFSET ' . $offset; 148 | return $this; 149 | } 150 | 151 | /** 152 | * Set the WHERE clause. 153 | * 154 | * @param string $col The column name. 155 | * @param string|null $operator The comparison operator. 156 | * @param mixed|null $value The value to compare. 157 | * @return CommandBuilderInterface 158 | */ 159 | public function where(string $col, string $operator = null, mixed $value = null): CommandBuilderInterface 160 | { 161 | $args = func_get_args(); 162 | 163 | if (empty($this->where)) { 164 | $this->where = ' WHERE '; 165 | } 166 | if (count($args) == 3) { 167 | if ($this->append_and_keyword) { 168 | $this->where .= ' AND '; 169 | } 170 | $this->where .= $col . " " . $operator . " '" . $value . "'"; 171 | $this->append_and_keyword = true; 172 | } else if (count($args) === 1) { 173 | $this->where .= ' ( '; 174 | $this->append_and_keyword = false; 175 | $this->append_or_keyword = false; 176 | $col($this); 177 | $this->where .= ' ) '; 178 | } 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * Set the OR WHERE clause. 185 | * 186 | * @param string $col The column name. 187 | * @param string|null $operator The comparison operator. 188 | * @param mixed|null $value The value to compare. 189 | * @return CommandBuilderInterface 190 | */ 191 | public function orWhere(string $col, string $operator = null, mixed $value = null): CommandBuilderInterface 192 | { 193 | $args = count(func_get_args()); 194 | if (empty($this->where)) { 195 | $this->where = " WHERE "; 196 | } 197 | if ($args === 3) { 198 | if ($this->append_or_keyword) 199 | $this->where .= ' OR '; 200 | $this->where .= $col . " " . $operator . " '" . $value . "'"; 201 | 202 | $this->append_or_keyword = true; 203 | } else if ($args === 1) { 204 | if ($this->where !== " WHERE ") 205 | $this->where .= " OR "; 206 | if ($this->append_or_keyword) 207 | $this->where .= " OR "; 208 | $this->where .= " ( "; 209 | $this->append_or_keyword = false; 210 | $this->append_and_keyword = false; 211 | $col($this); 212 | $this->where .= " ) "; 213 | } 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Set the WHERE IN clause. 220 | * 221 | * @param string $col The column name. 222 | * @param array $values The array of values. 223 | * @return CommandBuilderInterface 224 | */ 225 | public function whereIn(string $col, array $values): CommandBuilderInterface 226 | { 227 | if (empty($this->where)) { 228 | $this->where = " WHERE "; 229 | } 230 | if ($this->append_and_keyword || $this->append_or_keyword) { 231 | $this->where .= ' AND '; 232 | } 233 | $this->where .= $col . " IN ('" . implode("','", $values) . "')"; 234 | $this->append_and_keyword = true; 235 | return $this; 236 | } 237 | 238 | /** 239 | * Set the OR WHERE IN clause. 240 | * 241 | * @param string $col The column name. 242 | * @param array $values The array of values. 243 | * @return CommandBuilderInterface 244 | */ 245 | public function orWhereIn(string $col, array $values): CommandBuilderInterface 246 | { 247 | if (empty($this->where)) { 248 | $this->where = ' WHERE '; 249 | } 250 | if ($this->append_and_keyword || $this->append_or_keyword) { 251 | $this->where .= " OR "; 252 | } 253 | 254 | $this->where .= $col . " IN ('" . implode("','", $values) . "')"; 255 | $this->append_or_keyword = true; 256 | return $this; 257 | } 258 | 259 | /** 260 | * Build the SELECT command. 261 | * 262 | * @return CommandBuilderInterface 263 | */ 264 | private function buildSelectCommand() 265 | { 266 | if (empty($this->limit) && !empty($this->offset)) { 267 | $this->limit(PHP_INT_MAX); 268 | } 269 | 270 | $command = ''; 271 | if (count($this->select) === 0) { 272 | $command .= $this->table . ".*"; 273 | } else { 274 | $command = implode(',', $this->select); 275 | } 276 | 277 | $command = "SELECT " . $command . " FROM " . $this->table . " "; 278 | 279 | foreach ($this->select_command_clause as $clause) { 280 | if (!empty($this->$clause)) { 281 | $command .= $this->$clause . ' '; 282 | } 283 | } 284 | 285 | $command = rtrim($command); 286 | $this->commandString = $command; 287 | return $this; 288 | } 289 | 290 | /** 291 | * Join a table. 292 | * 293 | * @param string $table The table name. 294 | * @param string $col1 The column on the current table. 295 | * @param string $operator The join operator. 296 | * @param string $col2 The column on the joined table. 297 | * @return CommandBuilderInterface 298 | */ 299 | public function join(string $table, string $col1, string $operator, string $col2): CommandBuilderInterface 300 | { 301 | $this->join .= " INNER JOIN " . $table . " ON " . $col1 . " " . $operator . " " . $col2; 302 | return $this; 303 | } 304 | 305 | /** 306 | * Execute the SELECT query and return the results. 307 | * 308 | * @param string $class The class name to map the results to. 309 | * @return QueryResult 310 | */ 311 | public function get(string $class = \stdClass::class): QueryResult 312 | { 313 | $this->buildSelectCommand(); 314 | $db = DbConnection::getDb(); 315 | $data = $db->runQuery($this->commandString, [], $class); 316 | return $data; 317 | } 318 | 319 | /** 320 | * Calculate the average value of a column. 321 | * 322 | * @param string $col The column name. 323 | * @return mixed 324 | */ 325 | public function avg(string $col): mixed 326 | { 327 | $select = ["AVG(" . $col . ")"]; 328 | $this->select($select); 329 | $data = $this->get(); 330 | $obj = current($data->getRows()); 331 | $prop = current($select); 332 | return $obj->$prop; 333 | } 334 | 335 | /** 336 | * Calculate the sum of a column. 337 | * 338 | * @param string $col The column name. 339 | * @return mixed 340 | */ 341 | public function sum(string $col): mixed 342 | { 343 | $select = ["SUM(" . $col . ")"]; 344 | $this->select($select); 345 | $data = $this->get(); 346 | $obj = current($data->getRows()); 347 | $prop = current($select); 348 | return $obj->$prop; 349 | } 350 | 351 | /** 352 | * Count the number of rows. 353 | * 354 | * @param string $col The column name. 355 | * @return mixed 356 | */ 357 | public function count(string $col): mixed 358 | { 359 | $select = ["COUNT(" . $col . ")"]; 360 | $this->select($select); 361 | $data = $this->get(); 362 | $obj = current($data->getRows()); 363 | $prop = current($select); 364 | return $obj->$prop; 365 | } 366 | 367 | /** 368 | * Find the minimum value of a column. 369 | * 370 | * @param string $col The column name. 371 | * @return mixed 372 | */ 373 | public function min(string $col): mixed 374 | { 375 | $select = ["MIN(" . $col . ")"]; 376 | $this->select($select); 377 | $data = $this->get(); 378 | $obj = current($data->getRows()); 379 | $prop = current($select); 380 | return $obj->$prop; 381 | } 382 | 383 | /** 384 | * Find the maximum value of a column. 385 | * 386 | * @param string $col The column name. 387 | * @return mixed 388 | */ 389 | public function max(string $col): mixed 390 | { 391 | $select = ["MAX(" . $col . ")"]; 392 | $this->select($select); 393 | $data = $this->get(); 394 | $obj = current($data->getRows()); 395 | $prop = current($select); 396 | return $obj->$prop; 397 | } 398 | 399 | /** 400 | * Insert a new row into the table. 401 | * 402 | * @param array $data The data to be inserted. 403 | * @return int The number of affected rows. 404 | */ 405 | public function insert(array $data): int 406 | { 407 | $this->buildInsertCommand($data); 408 | $db = DbConnection::getDb(); 409 | return $db->runQuery($this->commandString)->getCount(); 410 | } 411 | 412 | /** 413 | * Insert a new row into the table and return the last inserted ID. 414 | * 415 | * @param array $data The data to be inserted. 416 | * @return string The last inserted ID. 417 | */ 418 | public function insertGetId(array $data): string 419 | { 420 | $this->buildInsertCommand($data); 421 | $db = DbConnection::getDb(); 422 | return $db->runQuery($this->commandString)->getLastInsertId(); 423 | } 424 | 425 | /** 426 | * Build the INSERT command. 427 | * 428 | * @param array $data The data to be inserted. 429 | * @return CommandBuilderInterface 430 | */ 431 | private function buildInsertCommand(array $data) 432 | { 433 | $col = []; 434 | if ($this->is_assoc($data)) { 435 | $col = array_keys($data); 436 | } else { 437 | $col = array_keys($data[0]); 438 | } 439 | $command = "INSERT INTO " . $this->table . " (" . implode(",", $col) . ") VALUES "; 440 | if ($this->is_assoc($data)) { 441 | $command .= "('" . implode("','", array_values($data)) . "')"; 442 | } else { 443 | foreach ($data as $row) { 444 | $command .= "('" . implode("','", array_values($row)) . "'),"; 445 | } 446 | $command = rtrim($command, ','); 447 | } 448 | $this->commandString = $command; 449 | return $this; 450 | } 451 | 452 | /** 453 | * Delete rows from the table. 454 | * 455 | * @return int The number of affected rows. 456 | */ 457 | public function delete(): int 458 | { 459 | $this->buildDeleteCommand(); 460 | return DbConnection::getDb()->runQuery($this->commandString)->getCount(); 461 | } 462 | 463 | /** 464 | * Build the DELETE command. 465 | * 466 | * @return CommandBuilderInterface 467 | */ 468 | private function buildDeleteCommand() 469 | { 470 | $command = "DELETE FROM " . $this->table; 471 | if (!empty($this->where)) { 472 | $command .= $this->where; 473 | } 474 | $this->commandString = $command; 475 | return $this; 476 | } 477 | 478 | /** 479 | * Update rows in the table. 480 | * 481 | * @param array $data The data to be updated. 482 | * @return int The number of affected rows. 483 | */ 484 | public function update(array $data): int 485 | { 486 | $this->buildUpdateCommand($data); 487 | return DbConnection::getDb()->runQuery($this->commandString)->getCount(); 488 | } 489 | 490 | /** 491 | * Build the UPDATE command. 492 | * 493 | * @param array $data The data to be updated. 494 | * @return CommandBuilderInterface 495 | */ 496 | private function buildUpdateCommand(array $data) 497 | { 498 | $command = "UPDATE " . $this->table . " SET "; 499 | foreach ($data as $key => $val) { 500 | $command .= $key . " = '" . $val . "',"; 501 | } 502 | $command = rtrim($command, ','); 503 | if (!empty($this->where)) { 504 | $command .= $this->where; 505 | } 506 | $this->commandString = $command; 507 | return $this; 508 | } 509 | 510 | /** 511 | * Get the command string. 512 | * 513 | * @return string The command string. 514 | */ 515 | public function getCommandString(): string 516 | { 517 | $this->buildSelectCommand(); 518 | return $this->commandString; 519 | } 520 | 521 | /** 522 | * Reset all the clauses and variables. 523 | * 524 | * @return void 525 | */ 526 | public function resetClause(): void 527 | { 528 | $this->table = ''; 529 | $this->select = []; 530 | $this->where = ''; 531 | $this->join = ''; 532 | $this->orderBy = ''; 533 | $this->groupBy = ''; 534 | $this->having = ''; 535 | $this->offset = ''; 536 | $this->limit = ''; 537 | $this->commandString = ''; 538 | $this->append_and_keyword = false; 539 | $this->append_or_keyword = false; 540 | } 541 | 542 | /** 543 | * Check if an array is associative. 544 | * 545 | * @param array $data The array to check. 546 | * @return bool 547 | */ 548 | private function is_assoc(array $data): bool 549 | { 550 | return array_keys($data) !== range(0, count($data) - 1); 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /App/DB/ORM/Builder.php: -------------------------------------------------------------------------------- 1 | cb = $command_builder; 25 | } 26 | 27 | /** 28 | * Get the command builder instance. 29 | * 30 | * @return CommandBuilderInterface The command builder instance. 31 | */ 32 | public function getCb(): CommandBuilderInterface 33 | { 34 | return $this->cb; 35 | } 36 | 37 | /** 38 | * Set the model for the query. 39 | * 40 | * @param mixed $model The model instance. 41 | * @return $this The Builder instance. 42 | */ 43 | public function model($model): self 44 | { 45 | $this->model = $model; 46 | return $this; 47 | } 48 | 49 | /** 50 | * Execute the query and get the results. 51 | * 52 | * @return array The query results. 53 | */ 54 | public function get(): array 55 | { 56 | $data = $this->cb->table($this->model->getTable())->get(get_class($this->model))->getRows(); 57 | if (count($data) && count($this->relations)) { 58 | foreach ($this->relations as $relationName => $relation) { 59 | $relation->buildRelationDataQuery($data); 60 | $relation_data = $relation->get(); 61 | $data = $relation->addRelationData($relationName, $data, $relation_data); 62 | } 63 | } 64 | 65 | return $data; 66 | } 67 | 68 | /** 69 | * Add a WHERE clause to the query. 70 | * 71 | * @return $this The Builder instance. 72 | */ 73 | public function where() 74 | { 75 | call_user_func_array([$this->cb, 'where'], func_get_args()); 76 | return $this; 77 | } 78 | 79 | /** 80 | * Add an OR WHERE clause to the query. 81 | * 82 | * @return $this The Builder instance. 83 | */ 84 | public function orWhere() 85 | { 86 | call_user_func_array([$this->cb, 'orWhere'], func_get_args()); 87 | return $this; 88 | } 89 | 90 | /** 91 | * Add a WHERE IN clause to the query. 92 | * 93 | * @return $this The Builder instance. 94 | */ 95 | public function whereIn() 96 | { 97 | call_user_func_array([$this->cb, 'whereIn'], func_get_args()); 98 | return $this; 99 | } 100 | 101 | /** 102 | * Add an OR WHERE IN clause to the query. 103 | * 104 | * @return $this The Builder instance. 105 | */ 106 | public function orWhereIn() 107 | { 108 | call_user_func_array([$this->cb, 'orWhereIn'], func_get_args()); 109 | return $this; 110 | } 111 | 112 | /** 113 | * Specify the columns to be selected in the query. 114 | * 115 | * @return $this The Builder instance. 116 | */ 117 | public function select() 118 | { 119 | call_user_func_array([$this->cb, 'select'], func_get_args()); 120 | return $this; 121 | } 122 | 123 | /** 124 | * Set the offset for the query. 125 | * 126 | * @return $this The Builder instance. 127 | */ 128 | public function offset() 129 | { 130 | call_user_func_array([$this->cb, 'offset'], func_get_args()); 131 | return $this; 132 | } 133 | 134 | /** 135 | * Set the limit for the query. 136 | * 137 | * @return $this The Builder instance. 138 | */ 139 | public function limit() 140 | { 141 | call_user_func_array([$this->cb, 'limit'], func_get_args()); 142 | return $this; 143 | } 144 | 145 | /** 146 | * Set the order of the query results. 147 | * 148 | * @return $this The Builder instance. 149 | */ 150 | public function orderBy() 151 | { 152 | call_user_func_array([$this->cb, 'orderBy'], func_get_args()); 153 | return $this; 154 | } 155 | 156 | /** 157 | * Perform a join operation on the query. 158 | * 159 | * @return $this The Builder instance. 160 | */ 161 | public function join() 162 | { 163 | call_user_func_array([$this->cb, 'join'], func_get_args()); 164 | return $this; 165 | } 166 | 167 | /** 168 | * Get the SQL string for the query. 169 | * 170 | * @return string The SQL string. 171 | */ 172 | public function toSql(): string 173 | { 174 | return $this->cb->table($this->model->getTable())->getCommandString(); 175 | } 176 | 177 | /** 178 | * Calculate the average of a column. 179 | * 180 | * @return mixed The average value. 181 | */ 182 | public function avg() 183 | { 184 | $this->cb->table($this->model->getTable()); 185 | return call_user_func_array([$this->cb, 'avg'], func_get_args()); 186 | } 187 | 188 | /** 189 | * Calculate the sum of a column. 190 | * 191 | * @return mixed The sum value. 192 | */ 193 | public function sum() 194 | { 195 | $this->cb->table($this->model->getTable()); 196 | return call_user_func_array([$this->cb, 'sum'], func_get_args()); 197 | } 198 | 199 | /** 200 | * Count the number of records. 201 | * 202 | * @return mixed The count value. 203 | */ 204 | public function count() 205 | { 206 | $this->cb->table($this->model->getTable()); 207 | return call_user_func_array([$this->cb, 'count'], func_get_args()); 208 | } 209 | 210 | /** 211 | * Get the maximum value of a column. 212 | * 213 | * @return mixed The maximum value. 214 | */ 215 | public function max() 216 | { 217 | $this->cb->table($this->model->getTable()); 218 | return call_user_func_array([$this->cb, 'max'], func_get_args()); 219 | } 220 | 221 | /** 222 | * Get the minimum value of a column. 223 | * 224 | * @return mixed The minimum value. 225 | */ 226 | public function min() 227 | { 228 | $this->cb->table($this->model->getTable()); 229 | return call_user_func_array([$this->cb, 'min'], func_get_args()); 230 | } 231 | 232 | /** 233 | * Get the first record from the query results. 234 | * 235 | * @return mixed|null The first record or null if not found. 236 | */ 237 | public function first() 238 | { 239 | $this->limit(1); 240 | 241 | $data = $this->get(); 242 | if (count($data) > 0) 243 | return current($data); 244 | 245 | return null; 246 | } 247 | 248 | /** 249 | * Find a record by its primary key value. 250 | * 251 | * @param mixed $id The primary key value. 252 | * @return mixed|null The found record or null if not found. 253 | */ 254 | public function find($id) 255 | { 256 | return $this->where($this->model->getPrimaryKey(), '=', $id)->first(); 257 | } 258 | 259 | /** 260 | * Create a new record in the database. 261 | * 262 | * @param array $data The data to be inserted. 263 | * @return mixed The created record. 264 | */ 265 | public function create(array $data) 266 | { 267 | $new_id = $this->cb->table($this->model->getTable())->insertGetId($data); 268 | return call_user_func_array([$this, 'find'], [$new_id]); 269 | } 270 | 271 | /** 272 | * Delete records from the database. 273 | * 274 | * @return mixed The number of affected rows. 275 | */ 276 | public function delete() 277 | { 278 | return $this->cb->table($this->model->getTable())->delete(); 279 | } 280 | 281 | /** 282 | * Update records in the database. 283 | * 284 | * @param array $data The data to be updated. 285 | * @return mixed The number of affected rows. 286 | */ 287 | public function update(array $data) 288 | { 289 | return $this->cb->table($this->model->getTable())->update($data); 290 | } 291 | 292 | /** 293 | * Eager load relationships for the query. 294 | * 295 | * @param array $relation The relationships to be loaded. 296 | * @return $this The Builder instance. 297 | */ 298 | public function with(array $relation = []) 299 | { 300 | foreach ($relation as $relationKeyOrName => $relationNameOrClosure) { 301 | if (is_string($relationNameOrClosure)) { 302 | $relation = call_user_func_array([$this->model, $relationNameOrClosure], []); 303 | $this->relations[$relationNameOrClosure] = $relation; 304 | } else { 305 | $relation = call_user_func_array([$this->model, $relationKeyOrName], []); 306 | $relationNameOrClosure($relation); 307 | $this->relations[$relationKeyOrName] = $relation; 308 | } 309 | } 310 | return $this; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /App/DB/ORM/Model.php: -------------------------------------------------------------------------------- 1 | model($model_instance); 39 | return call_user_func_array([$builder, $method], $arguments); 40 | } 41 | return call_user_func_array([$model, $method], $arguments); 42 | } 43 | 44 | /** 45 | * Get the primary key of the model. 46 | * 47 | * @return string The primary key column name. 48 | */ 49 | public function getPrimaryKey() 50 | { 51 | return $this->primary_key; 52 | } 53 | 54 | /** 55 | * Get the table name of the model. 56 | * 57 | * @return string The table name. 58 | */ 59 | public function getTable() 60 | { 61 | return $this->table; 62 | } 63 | 64 | /** 65 | * Remove the current record from the database. 66 | * 67 | * @return bool True if the record is successfully removed, false otherwise. 68 | */ 69 | public function remove() 70 | { 71 | $pk = $this->primary_key; 72 | if (!isset($this->primary_key)) { 73 | return false; 74 | } 75 | return DbConnection::getDb()->table($this->table)->where($this->primary_key, '=', $this->$pk)->delete(); 76 | } 77 | 78 | /** 79 | * Update the current record in the database. 80 | * 81 | * @param array $data The data to be updated. 82 | * @return bool True if the record is successfully updated, false otherwise. 83 | */ 84 | public function updateSingle(array $data) 85 | { 86 | $pk = $this->primary_key; 87 | if (!isset($this->$pk)) { 88 | return false; 89 | } 90 | return DbConnection::getDb()->table($this->table)->where($this->primary_key, '=', $this->$pk)->update($data); 91 | } 92 | 93 | /** 94 | * Save the model to the database. 95 | * 96 | * @return mixed The created or updated record. 97 | */ 98 | public function save() 99 | { 100 | $data = DbConnection::getDb()->runQuery('SHOW COLUMNS FROM ' . $this->table)->getRows(); 101 | $cols = array_column($data, 'Field'); 102 | 103 | foreach ($this->data as $key => $val) { 104 | $filtered = array_filter($cols, function ($col) use ($key) { 105 | return $col === $key; 106 | }); 107 | if (!count($filtered)) { 108 | unset($this->data[$key]); 109 | } 110 | } 111 | 112 | $pk = $this->primary_key; 113 | if (!is_null($this->$pk)) { 114 | return DbConnection::getDb()->table($this->table)->where($this->primary_key, '=', $this->$pk)->update($this->data); 115 | } else { 116 | $id = DbConnection::getDb()->table($this->table)->insertGetId($this->data); 117 | $this->primary_key = $id; 118 | return $id; 119 | } 120 | } 121 | 122 | /** 123 | * Define a has-many relationship. 124 | * 125 | * @param string $relationClass The class name of the related model. 126 | * @param string $foreignKey The foreign key column name in the related table. 127 | * @param string|null $localKey The local key column name in the current table (optional). 128 | * @return \App\DB\ORM\Relations\HasManyRelation The has-many relationship object. 129 | */ 130 | public function hasMany($relationClass, $foreignKey, $localKey = null) 131 | { 132 | $localKey = !empty($localKey) ? $localKey : $this->primary_key; 133 | $relation_model = new $relationClass; 134 | $relation = new HasManyRelation($this->table, $relation_model->getTable(), $foreignKey, $localKey); 135 | $relation->model($relation_model); 136 | $primary_key = $this->primary_key; 137 | 138 | if (isset($this->data[$primary_key])) { 139 | $relation->referenceModel($this); 140 | } 141 | 142 | $relation->initiateConnection(); 143 | return $relation; 144 | } 145 | 146 | /** 147 | * Define a belongs-to relationship. 148 | * 149 | * @param string $relationClass The class name of the related model. 150 | * @param string $foreignKey The foreign key column name in the current table. 151 | * @param string|null $localKey The local key column name in the related table (optional). 152 | * @return \App\DB\ORM\Relations\BelongsToRelation The belongs-to relationship object. 153 | */ 154 | public function belongsTo($relationClass, $foreignKey, $localKey = null) 155 | { 156 | $local_key = !empty($localKey) ? $localKey : $this->primary_key; 157 | $relation_model = new $relationClass(); 158 | $relation = new BelongsToRelation($this->table, $relation_model->getTable(), $foreignKey, $local_key); 159 | $relation->model($relation_model); 160 | $primary_key = $this->primary_key; 161 | 162 | if (isset($this->data[$primary_key])) { 163 | $relation->referenceModel($this); 164 | } 165 | 166 | $relation->initiateConnection(); 167 | return $relation; 168 | } 169 | 170 | /** 171 | * Define a has-one relationship. 172 | * 173 | * @param string $relationClass The class name of the related model. 174 | * @return \App\DB\ORM\Relations\HasOneRelation The has-one relationship object. 175 | */ 176 | public function hasOne($relationClass) 177 | { 178 | $primary_key = $this->primary_key; 179 | $relation_model = new $relationClass; 180 | $relation = new HasOneRelation($this->table, $relation_model->getTable(), $relation_model->getPrimaryKey(), $primary_key); 181 | $relation->model($relation_model); 182 | 183 | if (isset($this->data[$primary_key])) { 184 | $relation->referenceModel($this); 185 | } 186 | 187 | $relation->initiateConnection(); 188 | return $relation; 189 | } 190 | 191 | /** 192 | * Define a belongs-to-many relationship. 193 | * 194 | * @param string $relationClass The class name of the related model. 195 | * @param string $pivotTable The pivot table name. 196 | * @param string $referenceTableForeignKey The foreign key column name in the reference table. 197 | * @param string $relationTableForeignKey The foreign key column name in the relation table. 198 | * @param string|null $localKey The local key column name in the current table (optional). 199 | * @return \App\DB\ORM\Relations\BelongsToManyRelation The belongs-to-many relationship object. 200 | */ 201 | public function belongsToMany( 202 | $relationClass, 203 | $pivotTable, 204 | $referenceTableForeignKey, 205 | $relationTableForeignKey, 206 | $localKey = null 207 | ) { 208 | $primary_key = $this->primary_key; 209 | $local_key = !empty($localKey) ? $localKey : $primary_key; 210 | $relation_model = new $relationClass; 211 | $relation = new BelongsToManyRelation( 212 | $this->table, 213 | $pivotTable, 214 | $relation_model->getTable(), 215 | $referenceTableForeignKey, 216 | $relationTableForeignKey, 217 | $local_key, 218 | $relation_model->getPrimaryKey() 219 | ); 220 | $relation->model($relation_model); 221 | 222 | if (isset($this->data[$primary_key])) { 223 | $relation->referenceModel($this); 224 | } 225 | 226 | $relation->initiateConnection(); 227 | return $relation; 228 | } 229 | 230 | /** 231 | * Magic method to set values to the model's properties. 232 | * 233 | * @param string $name The property name. 234 | * @param mixed $value The property value. 235 | */ 236 | public function __set($name, $value) 237 | { 238 | $this->data[$name] = $value; 239 | } 240 | 241 | /** 242 | * Magic method to get values from the model's properties. 243 | * 244 | * @param string $name The property name. 245 | * @return mixed|null The property value if it exists, null otherwise. 246 | */ 247 | public function __get($name) 248 | { 249 | return $this->data[$name] ?? null; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /App/DB/ORM/Relations/BelongsToManyRelation.php: -------------------------------------------------------------------------------- 1 | reference_table = $referenceTable; 47 | $this->relation_table = $relationTable; 48 | $this->pivot_table = $pivotTable; 49 | $this->reference_table_foreignKey = $referenceTableForeignKey; 50 | $this->relation_table_foreignKey = $relationTableForeignKey; 51 | $this->reference_table_primaryKey = $referenceTableLocalKey; 52 | $this->relation_table_primaryKey = $relationTableLocalKey; 53 | } 54 | 55 | /** 56 | * Add pivot table data to the result. 57 | * 58 | * @param array $data The data to add pivot table data to. 59 | * @return array The data with added pivot table data. 60 | */ 61 | public function addPivotData($data) 62 | { 63 | if (count($this->pivot_columns)) { 64 | foreach ($data as $key => $data_object) { 65 | $pivot = new \stdClass; 66 | foreach ($this->pivot_columns as $col) { 67 | $pivot->$col = $data_object->$col; 68 | unset($data_object->$col); 69 | } 70 | $data_object->pivot = clone $pivot; 71 | $data[$key] = $data_object; 72 | } 73 | } 74 | 75 | return $data; 76 | } 77 | 78 | /** 79 | * Get the first record from the result set with pivot table data. 80 | * 81 | * @return mixed The first record with pivot table data. 82 | */ 83 | public function first() 84 | { 85 | $model = parent::first(); 86 | $data = $this->addPivotData([$model]); 87 | 88 | return current($data); 89 | } 90 | 91 | /** 92 | * Get all the records from the result set with pivot table data. 93 | * 94 | * @return array All the records with pivot table data. 95 | */ 96 | public function get(): array 97 | { 98 | $data = parent::get(); 99 | return $this->addPivotData($data); 100 | } 101 | 102 | /** 103 | * Build the query to retrieve the relation data. 104 | * 105 | * @param array $data The data to build the query for. 106 | * @return $this The current BelongsToManyRelation instance. 107 | */ 108 | public function buildRelationDataQuery($data) 109 | { 110 | $primary_key = $this->reference_table_primaryKey; 111 | $ids = array_map(function ($data) use ($primary_key) { 112 | return $data->{$primary_key}; 113 | }, $data); 114 | $this->whereIn($this->pivot_table . '.' . $this->reference_table_foreignKey, $ids); 115 | 116 | return $this; 117 | } 118 | /** 119 | * Add relation data to the reference model. 120 | * 121 | * @param string $relationName The name of the relation. 122 | * @param array $data The reference model data. 123 | * @param array $relation_data The relation data. 124 | * @return array The reference model data with added relation data. 125 | */ 126 | public function addRelationData($relationName, $data, $relation_data) 127 | { 128 | $reference_table_primaryKey = $this->reference_table_primaryKey; 129 | $reference_table_foreignKey = $this->reference_table_foreignKey; 130 | foreach ($data as $key => $reference_model) { 131 | $filtered_data = array_filter( 132 | $relation_data, 133 | function ($relation_obj) use ($reference_table_primaryKey, $reference_table_foreignKey, $reference_model) { 134 | return $reference_model->$reference_table_primaryKey == $relation_obj->pivot->$reference_table_foreignKey; 135 | } 136 | ); 137 | $reference_model->$relationName = $filtered_data; 138 | $data[$key] = $reference_model; 139 | } 140 | return $data; 141 | } 142 | 143 | /** 144 | * Specify the columns to include from the pivot table. 145 | * 146 | * @param string|array $cols The pivot table columns to include. 147 | * @return $this The current BelongsToManyRelation instance. 148 | */ 149 | public function withPivot($cols) 150 | { 151 | $cols = is_array($cols) ? $cols : [$cols]; 152 | foreach ($cols as $col) { 153 | array_push($this->pivot_columns, $col); 154 | } 155 | 156 | if (count($this->pivot_columns)) { 157 | $this->select($this->model->getTable() . '.*'); 158 | foreach ($this->pivot_columns as $col) { 159 | $this->select($this->pivot_table . '.' . $col); 160 | } 161 | } 162 | return $this; 163 | } 164 | 165 | /** 166 | * Attach records to the pivot table. 167 | * 168 | * @param array $data The data to attach to the pivot table. 169 | * @return int The number of affected rows. 170 | */ 171 | public function attach($data) 172 | { 173 | $insertable_data = []; 174 | $referenceModel = $this->referenceModel; 175 | $reference_table_primaryKey = $this->reference_table_primaryKey; 176 | 177 | foreach ($data as $key => $value) { 178 | $insertable_row = []; 179 | $insertable_row[$this->reference_table_foreignKey] = $referenceModel->$reference_table_primaryKey; 180 | 181 | if (is_array($value)) { 182 | $insertable_row[$this->relation_table_foreignKey] = $key; 183 | foreach ($value as $vk => $vv) { 184 | $insertable_row[$vk] = $vv; 185 | } 186 | } else { 187 | $insertable_row[$this->relation_table_foreignKey] = $value; 188 | } 189 | array_push($insertable_data, $insertable_row); 190 | } 191 | $affected_row = 0; 192 | foreach ($insertable_data as $row) { 193 | $res = $this->cb->table($this->pivot_table)->insert($row); 194 | $affected_row++; 195 | } 196 | return $affected_row; 197 | } 198 | 199 | /** 200 | * Detach records from the pivot table. 201 | * 202 | * @param array $data The data to detach from the pivot table. 203 | * @return int The number of deleted rows. 204 | */ 205 | public function detach(array $data) 206 | { 207 | $reference_model = $this->referenceModel; 208 | $reference_table_primaryKey = $this->reference_table_primaryKey; 209 | 210 | $cb = $this->cb->table($this->pivot_table); 211 | $cb->where($this->reference_table_foreignKey, '=', $reference_model->$reference_table_primaryKey); 212 | $cb->whereIn($this->relation_table_foreignKey, $data); 213 | return $cb->delete(); 214 | } 215 | 216 | /** 217 | * Initiate the connection for the relation. 218 | * 219 | * @return $this The current BelongsToManyRelation instance. 220 | */ 221 | public function initiateConnection() 222 | { 223 | if (!$this->connectionInitiated) { 224 | $this->join($this->pivot_table, $this->pivot_table . '.' . $this->relation_table_foreignKey, '=', $this->relation_table . '.' . $this->relation_table_primaryKey); 225 | 226 | $this->join($this->reference_table, $this->pivot_table . '.' . $this->reference_table_foreignKey, '=', $this->reference_table . '.' . $this->reference_table_primaryKey); 227 | $this->connectionInitiated = true; 228 | } 229 | $referenceModel = $this->referenceModel; 230 | if (!empty($this->referenceModel)) { 231 | $reference_table_primaryKey = $this->reference_table_primaryKey; 232 | $this->where($this->pivot_table . '.' . $this->reference_table_foreignKey, '=', $referenceModel->$reference_table_primaryKey); 233 | } 234 | $this->withPivot($this->reference_table_foreignKey); 235 | return $this; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /App/DB/ORM/Relations/BelongsToRelation.php: -------------------------------------------------------------------------------- 1 | reference_table = $reference_table; 45 | $this->relation_table = $relation_table; 46 | $this->foreign_key = $foreign_key; 47 | $this->local_key = $local_key; 48 | } 49 | 50 | /** 51 | * Initializes the database connection and sets up the query conditions based on the reference model. 52 | * 53 | * @return $this 54 | */ 55 | public function initiateConnection() 56 | { 57 | $reference_model = $this->referenceModel; 58 | if (!$this->connectionInitiated && !empty($reference_model)) { 59 | $foreign_key = $this->foreign_key; 60 | $this->where($this->relation_table . '.' . $this->local_key, '=', $reference_model->$foreign_key); 61 | $this->connectionInitiated = true; 62 | } 63 | return $this; 64 | } 65 | 66 | /** 67 | * Builds the query to fetch the relation data based on the given data. 68 | * 69 | * @param array $data The reference model data. 70 | * @return void 71 | */ 72 | public function buildRelationDataQuery($data) 73 | { 74 | $ids = array_map(function ($item) { 75 | return $item->{$this->local_key}; 76 | }, $data); 77 | $this->whereIn($this->relation_table . '.' . $this->foreign_key, $ids); 78 | } 79 | 80 | /** 81 | * Adds the relation data to the reference models. 82 | * 83 | * @param string $relationName The name of the relation. 84 | * @param array $data The reference model data. 85 | * @param array $relation_data The relation data. 86 | * @return array The updated reference model data. 87 | */ 88 | public function addRelationData($relationName, $data, $relation_data) 89 | { 90 | $local_key = $this->local_key; 91 | $foreign_key = $this->foreign_key; 92 | foreach ($data as $key => $reference_model) { 93 | $filtered_relation_data = array_filter($relation_data, function ($relation_data_obj) use ($local_key, $foreign_key, $reference_model) { 94 | return $reference_model->$foreign_key === $relation_data_obj->$local_key; 95 | }); 96 | if (count($filtered_relation_data)) { 97 | $reference_model->$relationName = current($filtered_relation_data); 98 | } else { 99 | $reference_model->$relationName = null; 100 | } 101 | $data[$key] = $reference_model; 102 | } 103 | return $data; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /App/DB/ORM/Relations/HasManyRelation.php: -------------------------------------------------------------------------------- 1 | referenceTable = $referenceTable; 50 | $this->localKey = $localKey; 51 | $this->foreignKey = $foreignKey; 52 | $this->relationTable = $relationTable; 53 | } 54 | 55 | /** 56 | * Initializes the database connection and sets up the query conditions based on the reference model. 57 | * 58 | * @return $this 59 | */ 60 | public function initiateConnection() 61 | { 62 | $referenceModel = $this->referenceModel; 63 | if (!$this->connectionInitiated && !empty($referenceModel)) { 64 | $localKey = $this->localKey; 65 | $this->where($this->relationTable . '.' . $this->foreignKey, '=', $referenceModel->$localKey); 66 | $this->connectionInitiated = true; 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Builds the query to fetch the relation data based on the given data. 74 | * 75 | * @param array $data The reference model data. 76 | * @return $this 77 | */ 78 | public function buildRelationDataQuery($data) 79 | { 80 | $ids = array_map(function ($item) { 81 | return $item[$this->localKey]; 82 | }, $data); 83 | $this->whereIn($this->relationTable . '.' . $this->foreignKey, $ids); 84 | return $this; 85 | } 86 | 87 | /** 88 | * Adds the relation data to the reference models. 89 | * 90 | * @param string $relationName The name of the relation. 91 | * @param array $data The reference model data. 92 | * @param array $relation_data The relation data. 93 | * @return array The updated reference model data. 94 | */ 95 | public function addRelationData($relationName, $data, $relation_data) 96 | { 97 | $localKey = $this->localKey; 98 | $foreignKey = $this->foreignKey; 99 | foreach ($data as $key => $referenceModel) { 100 | $filtered_relation_data = array_filter($relation_data, function ($relation_data_object) use ($foreignKey, $localKey, $referenceModel) { 101 | return $referenceModel->$localKey == $relation_data_object->$foreignKey; 102 | }); 103 | $referenceModel->$relationName = $filtered_relation_data; 104 | $data[$key] = $referenceModel; 105 | } 106 | return $data; 107 | } 108 | 109 | /** 110 | * Creates a new record in the relation table associated with the reference model. 111 | * 112 | * @param array $data The data to be inserted into the relation table. 113 | * @return mixed The result of the create operation. 114 | */ 115 | public function create($data) 116 | { 117 | $foreign_key = $this->foreignKey; 118 | $local_key = $this->localKey; 119 | $referenceModel = $this->referenceModel; 120 | $data[$foreign_key] = $referenceModel->$local_key; 121 | return parent::create($data); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /App/DB/ORM/Relations/HasOneRelation.php: -------------------------------------------------------------------------------- 1 | reference_table = $reference_table; 25 | $this->relation_table = $relation_table; 26 | $this->foreign_key = $foreign_key; 27 | $this->local_key = $local_key; 28 | } 29 | 30 | /** 31 | * Initialize the connection and set up the query conditions. 32 | * 33 | * @return $this 34 | */ 35 | public function initiateConnection() 36 | { 37 | $reference_model = $this->referenceModel; 38 | if (!$this->connectionInitiated && !empty($reference_model)) { 39 | $local_key = $this->local_key; 40 | $this->where($this->relation_table . '.' . $this->foreign_key, '=', $reference_model->$local_key); 41 | $this->connectionInitiated = true; 42 | } 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Build the query to retrieve the relation data. 49 | * 50 | * @param array $data The data of the reference model. 51 | * @return void 52 | */ 53 | public function buildRelationDataQuery($data) 54 | { 55 | $ids = array_map(function ($item) { 56 | return $item[$this->local_key]; 57 | }, $data); 58 | $this->whereIn($this->relation_table . '.' . $this->foreign_key, $ids); 59 | } 60 | 61 | /** 62 | * Add the relation data to the reference models. 63 | * 64 | * @param string $relationName The name of the relation. 65 | * @param array $data The data of the reference models. 66 | * @param array $relation_data The relation data. 67 | * @return array The updated reference models. 68 | */ 69 | public function addRelationData($relationName, $data, $relation_data) 70 | { 71 | $foreign_key = $this->foreign_key; 72 | $local_key = $this->local_key; 73 | foreach ($data as $key => $reference_model) { 74 | $filtered_relation_data = array_filter($relation_data, function ($relation_data_obj) use ($reference_model, $foreign_key, $local_key) { 75 | return $reference_model->$local_key === $relation_data_obj->$foreign_key; 76 | }); 77 | if (count($filtered_relation_data)) { 78 | $reference_model->$relationName = current($filtered_relation_data); 79 | } else { 80 | $reference_model->$relationName = null; 81 | } 82 | $data[$key] = $reference_model; 83 | } 84 | return $data; 85 | } 86 | 87 | /** 88 | * Create a new record in the relation table associated with the reference model. 89 | * 90 | * @param array $data The data to be inserted into the relation table. 91 | * @return mixed The result of the create operation. 92 | */ 93 | public function create($data) 94 | { 95 | $foreign_key = $this->foreign_key; 96 | $local_key = $this->local_key; 97 | $referenceModel = $this->referenceModel; 98 | $data[$foreign_key] = $referenceModel->$local_key; 99 | return parent::create($data); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /App/DB/ORM/Relations/Relation.php: -------------------------------------------------------------------------------- 1 | referenceModel; 26 | } 27 | 28 | /** 29 | * Set the reference model instance. 30 | * 31 | * @param mixed $referenceModel 32 | * @return $this 33 | */ 34 | public function referenceModel($referenceModel) 35 | { 36 | $this->referenceModel = $referenceModel; 37 | return $this; 38 | } 39 | 40 | /** 41 | * Initiate the connection for the relation. 42 | * 43 | * @return $this 44 | */ 45 | public abstract function initiateConnection(); 46 | 47 | /** 48 | * Build the query to fetch the relation data. 49 | * 50 | * @param array $data 51 | * @return $this 52 | */ 53 | public abstract function buildRelationDataQuery($data); 54 | 55 | /** 56 | * Add the relation data to the reference models. 57 | * 58 | * @param string $relationName 59 | * @param array $data 60 | * @param array $relation_data 61 | * @return array 62 | */ 63 | public abstract function addRelationData($relationName, $data, $relation_data); 64 | } 65 | -------------------------------------------------------------------------------- /App/DB/pdo.php: -------------------------------------------------------------------------------- 1 | pdo = new \PDO($dsn, $username, $password, $options); 32 | $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 33 | $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); 34 | } catch (PDOException $e) { 35 | throw new ConnectionException("Failed to connect to the database: {$e->getMessage()}", $e->getCode(), $e); 36 | } 37 | } 38 | 39 | /** 40 | * Executes a SQL query and returns the result. 41 | * 42 | * @param string $sql The SQL query. 43 | * @param array $params The query parameters. 44 | * @param string $class The class name to instantiate the result objects. 45 | * @return QueryResult The query result. 46 | * @throws QueryException If the query execution fails. 47 | */ 48 | public function runQuery(string $sql, array $params = [], string $class = \stdClass::class): QueryResult 49 | { 50 | try { 51 | $stmt = $this->prepare($sql); 52 | $stmt->execute($params); 53 | $stmt->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, $class); 54 | $rows = $stmt->fetchAll(); 55 | $count = $stmt->rowCount(); 56 | $lastInsertId = $this->pdo->lastInsertId(); 57 | $this->queryResult = new QueryResult($rows, $count, $lastInsertId); 58 | 59 | return $this->queryResult; 60 | } catch (PDOException $e) { 61 | throw new QueryException($e->getMessage()); 62 | } 63 | } 64 | 65 | /** 66 | * Prepares an SQL statement for execution. 67 | * 68 | * @param string $sql The SQL statement. 69 | * @return \PDOStatement The prepared statement. 70 | * @throws QueryException If the statement preparation fails. 71 | */ 72 | public function prepare(string $sql): \PDOStatement 73 | { 74 | try { 75 | return $this->pdo->prepare($sql); 76 | } catch (PDOException $e) { 77 | throw new QueryException("Failed to prepare the query: {$e->getMessage()}", $e->getCode(), $e); 78 | } 79 | } 80 | 81 | /** 82 | * Returns the ID of the last inserted row or sequence value. 83 | * 84 | * @param string|null $name The name of the sequence object from which the ID should be returned. 85 | * @return string The last insert ID. 86 | */ 87 | public function getLastInsertId(string $name = null): string 88 | { 89 | return $this->pdo->lastInsertId($name); 90 | } 91 | 92 | /** 93 | * Returns the number of rows affected by the last executed query. 94 | * 95 | * @return int The number of affected rows. 96 | */ 97 | public function countAffected(): int 98 | { 99 | return $this->queryResult->getCount(); 100 | } 101 | 102 | /** 103 | * Checks if the database connection is active. 104 | * 105 | * @return bool True if the connection is active, false otherwise. 106 | */ 107 | public function isConnected(): bool 108 | { 109 | // Check if the PDO connection is active 110 | $connectionStatus = $this->pdo->getAttribute(\PDO::ATTR_CONNECTION_STATUS); 111 | 112 | if ($connectionStatus === 'Connected') { 113 | return true; 114 | } 115 | 116 | return false; 117 | } 118 | } 119 | 120 | /** 121 | * The QueryResult class represents the result of a database query. 122 | */ 123 | class QueryResult 124 | { 125 | private array $rows; 126 | private int $count; 127 | private string $lastInsertId; 128 | 129 | /** 130 | * QueryResult constructor. 131 | * 132 | * @param array $rows The query result rows. 133 | * @param int $count The number of rows affected by the query. 134 | * @param string $lastInsertId The ID of the last inserted row or sequence value. 135 | */ 136 | public function __construct(array $rows, int $count, string $lastInsertId) 137 | { 138 | $this->rows = $rows; 139 | $this->count = $count; 140 | $this->lastInsertId = $lastInsertId; 141 | } 142 | 143 | /** 144 | * Get the rows of the query result. 145 | * 146 | * @return array The query result rows. 147 | */ 148 | public function getRows(): array 149 | { 150 | return $this->rows; 151 | } 152 | 153 | /** 154 | * Get the number of rows affected by the query. 155 | * 156 | * @return int The number of rows affected. 157 | */ 158 | public function getCount(): int 159 | { 160 | return $this->count; 161 | } 162 | 163 | /** 164 | * Get the ID of the last inserted row or sequence value. 165 | * 166 | * @return string The last insert ID. 167 | */ 168 | public function getLastInsertId(): string 169 | { 170 | return $this->lastInsertId; 171 | } 172 | } 173 | 174 | /** 175 | * The QueryException class represents an exception that occurs during a database query. 176 | */ 177 | class QueryException extends \RuntimeException 178 | { 179 | } 180 | 181 | /** 182 | * The ConnectionException class represents an exception that occurs when connecting to the database fails. 183 | */ 184 | class ConnectionException extends \RuntimeException 185 | { 186 | } 187 | -------------------------------------------------------------------------------- /App/Hook/AfterControllerHook.php: -------------------------------------------------------------------------------- 1 | 0)); 18 | 19 | /** 20 | * Adds a hook to be executed after the controller. 21 | * 22 | * @param string $hookName The name of the hook. 23 | * @param callable $callback The callback function to be executed. 24 | * @param array $options Additional options for the hook. 25 | * Available options: 26 | * - priority: The priority of the hook (default: 10). 27 | * @return void 28 | */ 29 | $hook::addHook('afterController', function () { 30 | // echo 'after controller hook2 /'; 31 | // $file = dirname(__DIR__) . '/../log/2023-06-11.txt'; 32 | // error_log('after controller 2', 3, $file); 33 | }); 34 | -------------------------------------------------------------------------------- /App/Hook/BeforeControllerHook.php: -------------------------------------------------------------------------------- 1 | 0)); 34 | -------------------------------------------------------------------------------- /App/Hook/CustomControllerHook.php: -------------------------------------------------------------------------------- 1 | 'before')); 19 | 20 | /** 21 | * Adds a custom hook to be executed before or after the "apples/index" action. 22 | * 23 | * @param string $hookName The name of the hook. 24 | * @param callable $callback The callback function to be executed. 25 | * @param array $options Additional options for the hook. 26 | * Available options: 27 | * - runwhen: Specifies when the hook should run ("before" or "after"). 28 | * @return void 29 | */ 30 | $hook::addCustomHook('home/index', function () { 31 | // echo 'home index custom before hook2 /'; 32 | // $file = dirname(__DIR__) . '/../log/2023-06-11.txt'; 33 | // error_log('action hook2', 3, $file); 34 | }, array('runwhen' => 'before')); 35 | 36 | /** 37 | * Adds a custom hook to be executed before or after the "apples/index" action. 38 | * 39 | * @param string $hookName The name of the hook. 40 | * @param callable $callback The callback function to be executed. 41 | * @param array $options Additional options for the hook. 42 | * Available options: 43 | * - runwhen: Specifies when the hook should run ("before" or "after"). 44 | * @return void 45 | */ 46 | $hook::addCustomHook('home/index', function () { 47 | // echo 'home index custom after hook1 /'; 48 | // $file = dirname(__DIR__) . '/../log/2023-06-11.txt'; 49 | // error_log('action hook3', 3, $file); 50 | }, array('runwhen' => 'after')); 51 | -------------------------------------------------------------------------------- /App/Model/Post.php: -------------------------------------------------------------------------------- 1 | belongsTo('App\Model\User', 'userId'); 29 | } 30 | 31 | // Uncomment the code below to enable the `getAll` method. 32 | 33 | // /** 34 | // * Get all the posts. 35 | // * 36 | // * @return array 37 | // */ 38 | // public static function getAll() 39 | // { 40 | // try { 41 | // $db = static::getDb(); 42 | // // Add your logic here to fetch all the posts 43 | // // and return them as an array. 44 | // } catch (PDOException $e) { 45 | // // Handle the exception if necessary. 46 | // } 47 | // } 48 | } 49 | -------------------------------------------------------------------------------- /App/Model/Profile.php: -------------------------------------------------------------------------------- 1 | belongsTo('App\Model\User', 'id'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /App/Model/User.php: -------------------------------------------------------------------------------- 1 | hasOne('App\Model\Profile'); 29 | } 30 | 31 | /** 32 | * Get the posts associated with the user. 33 | * 34 | * @return \App\Model\Post[] 35 | */ 36 | public function posts() 37 | { 38 | return $this->hasMany('App\Model\Post', 'userId'); 39 | } 40 | 41 | /** 42 | * Get the posts that the user has rated. 43 | * 44 | * @return \App\Model\Post[] 45 | */ 46 | public function ratedPosts() 47 | { 48 | return $this->belongsToMany('App\Model\Post', 'ratings', 'userId', 'postId')->withPivot('ratings'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /App/Routes.php: -------------------------------------------------------------------------------- 1 | add('', ['controller' => 'home', 'action' => 'index']); 5 | $router->add('{controller}/{action}'); 6 | $router->add('{controller}'); 7 | $router->add('admin/{controller}/{action}', ['namespace' => 'Admin']); 8 | $router->add('{controller}/{id:\d+}/{action}'); 9 | -------------------------------------------------------------------------------- /App/Service/TestService.php: -------------------------------------------------------------------------------- 1 | has('db')) { 17 | $di->set('db', new \Engine\Db($di)); 18 | } 19 | $this->db = $di->get('db'); 20 | $this->table = \App\Config::session_db_table; 21 | $this->di = $di; 22 | } 23 | 24 | public function read(string $session_id): array 25 | { 26 | $stmt = $this->db->prepare("SELECT data from {$this->table} WHERE session_id = ?"); 27 | $stmt->execute([$session_id]); 28 | $data = $stmt->fetch(PDO::FETCH_COLUMN); 29 | return $data ? unserialize($data) : []; 30 | } 31 | 32 | public function write(string $session_id, array $data): bool 33 | { 34 | $serializedData = serialize($data); 35 | try { 36 | $stmt = $this->db->prepare("REPLACE INTO {$this->table} (session_id, data) VALUES (?, ?)"); 37 | return $stmt->execute([$session_id, $serializedData]); 38 | } catch (\PDOException $e) { 39 | // handle error 40 | return false; 41 | } 42 | } 43 | 44 | public function destroy(string $session_id): bool 45 | { 46 | try { 47 | 48 | $stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE session_id = ?"); 49 | return $stmt->execute([$session_id]); 50 | } catch (\PDOException $e) { 51 | return false; 52 | } 53 | } 54 | 55 | public function gc(string $lifetime): bool 56 | { 57 | $maxLifetime = time() - $lifetime; 58 | try { 59 | $stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE last_activity < ?"); 60 | $stmt->execute([$maxLifetime]); 61 | } catch (\PDOException $e) { 62 | return false; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /App/Session/redis.php: -------------------------------------------------------------------------------- 1 | redis = new \Redis(); 18 | $this->redis->connect(\App\Config::session_redis_host, \App\Config::session_redis_port); 19 | // $this->redis = new RedisClient([ 20 | // 'schema' => $schema, 21 | // 'host' => $hostname, 22 | // 'port' => $port, 23 | // 'password' => $password, 24 | // 'persistent' => $persistent 25 | // ]); 26 | 27 | $this->di = $di; 28 | } 29 | 30 | public function read(string $session_id): array 31 | { 32 | $data = $this->redis->get($session_id); 33 | 34 | return $data ? json_decode($data, true) : []; 35 | } 36 | 37 | public function write(string $session_id, array $data): bool 38 | { 39 | return $this->redis->set($session_id, json_encode($data), \App\Config::session_expire); 40 | } 41 | 42 | public function gc(string $max_lifetime): bool 43 | { 44 | // Redis automatically expires keys, so we don't need to do anything here. 45 | return true; 46 | } 47 | 48 | public function destroy(string $session_id): bool 49 | { 50 | return $this->redis->del($session_id); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /App/View/default/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block body %} 6 | 7 |
Sorry, that page doesn't exist.
9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /App/View/default/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Error{% endblock %} 4 | 5 | {% block body %} 6 | 7 |Sorry, an error has occurred.
9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /App/View/default/Home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}abc{% endblock %} 4 | 5 | {% block body %} 6 | 7 |Uncaught exception: '" . get_class($exception) . "'
"; 51 | echo "Message: '" . $exception->getMessage() . "'
"; 52 | echo "Stack trace:
" . $exception->getTraceAsString() . ""; 53 | echo "
Thrown in '" . $exception->getFile() . "' on line " . $exception->getLine() . "
"; 54 | } else { 55 | $log = dirname(__DIR__) . '/log/' . date('Y-m-d') . '.txt'; 56 | ini_set('error_log', $log); 57 | $message = "Uncaught exception: '" . get_class($exception) . "'"; 58 | $message .= " with message '" . $exception->getMessage() . "'"; 59 | $message .= "\n Stack trace: " . $exception->getTraceAsString(); 60 | $message .= "\n Thrown in '" . $exception->getFile() . "' on line " . $exception->getLine(); 61 | error_log($message); 62 | View::renderTemplate("$code.html"); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Core/Model.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 21 | } 22 | 23 | /** 24 | * Attempts to match the given URL against the defined routes. 25 | * 26 | * @param string $url The URL to match. 27 | * @return bool True if a match is found, false otherwise. 28 | */ 29 | public function match($url) 30 | { 31 | $url = $this->removeQueryStringVariables($url); 32 | foreach ($this->routes as $route => $params) { 33 | if (preg_match($route, $url, $matches)) { 34 | foreach ($matches as $key => $match) { 35 | if (is_string($key)) { 36 | $params[$key] = $match; 37 | } 38 | } 39 | $this->params = $params; 40 | return true; 41 | } 42 | } 43 | return false; 44 | } 45 | 46 | /** 47 | * Converts the controller name to camelCase. 48 | * 49 | * @param string $controller The controller name. 50 | * @return string The camelCase version of the controller name. 51 | */ 52 | private function controllerNameCamel($controller) 53 | { 54 | return str_replace(' ', '', ucwords(str_replace('-', ' ', $controller))); 55 | } 56 | 57 | /** 58 | * Converts the method name to camelCase. 59 | * 60 | * @param string $methodName The method name. 61 | * @return string The camelCase version of the method name. 62 | */ 63 | private function methodNameCamel($methodName) 64 | { 65 | return lcfirst($this->controllerNameCamel($methodName)); 66 | } 67 | 68 | /** 69 | * Dispatches the request to the appropriate controller and method based on the matched route. 70 | * 71 | * @param string $url The URL to dispatch. 72 | * @throws \Exception If the controller or method is not found. 73 | */ 74 | public function dispatch($url) 75 | { 76 | $url = rtrim($url, '/'); 77 | // if (empty($url)) { 78 | // $url = 'home'; 79 | // } 80 | if ($this->match($url)) { 81 | $controller = $this->params['controller']; 82 | $controller = $this->controllerNameCamel($controller); 83 | $controller = $this->getNamespace() . $controller; 84 | if (class_exists($controller)) { 85 | $this->params['action'] = isset($this->params['action']) ? $this->params['action'] : 'index'; 86 | 87 | $controller_object = new $controller($this->params, $this->registry); 88 | $this->registry->set('currentRoute', $controller_object); 89 | $method = $this->params['action']; 90 | $method = $this->methodNameCamel($method); 91 | 92 | // prevent the user call the action directory from the url such as http://localhost/controller/indexAction 93 | if (preg_match('/action$/i', $method) == 0) { 94 | $controller_object->$method($this->params); 95 | } else { 96 | throw new \Exception("Method $method in controller $controller cannot be called directly"); 97 | } 98 | } else { 99 | throw new \Exception("Controller $controller not found"); 100 | } 101 | } else { 102 | throw new \Exception('Router not found', 404); 103 | } 104 | } 105 | 106 | /** 107 | * Retrieves the namespace for the controllers. 108 | * 109 | * @return string The namespace for the controllers. 110 | */ 111 | protected function getNamespace() 112 | { 113 | $namespace = 'App\\Controller\\'; 114 | if (isset($this->params['namespace'])) { 115 | return $namespace . $this->params['namespace'] . '\\'; 116 | } 117 | return $namespace; 118 | } 119 | 120 | /** 121 | * Removes the query string variables from the URL. 122 | * 123 | * @param string $url The URL to remove the query string variables from 124 | * @return string The processed URL. 125 | */ 126 | protected function removeQueryStringVariables($url) 127 | { 128 | if ($url != '') { 129 | $parts = explode('&', $url, 2); 130 | 131 | if (strpos($parts[0], '=') === false) { 132 | $url = $parts[0]; 133 | } else { 134 | $url = ''; 135 | } 136 | } 137 | 138 | return $url; 139 | // return parse_url($url, PHP_URL_PATH) ?: ''; 140 | } 141 | 142 | /** 143 | * Adds a new route to the router. 144 | * 145 | * @param string $route The route pattern. 146 | * @param array $params The route parameters. 147 | */ 148 | public function add($route, $params = []) 149 | { 150 | $route = preg_replace('/\//', '\\/', $route); 151 | $route = preg_replace('/\{([a-z]+)\}/', '(?P<\1>[a-z-]+)', $route); 152 | $route = preg_replace('/\{([a-z]+):([^\}]+)\}/', '(?P<\1>\2)', $route); 153 | 154 | $route = '/^' . $route . '$/i'; 155 | $this->routes[$route] = $params; 156 | } 157 | 158 | /** 159 | * Retrieves the route parameters. 160 | * 161 | * @return array The route parameters. 162 | */ 163 | public function getParams() 164 | { 165 | return $this->params; 166 | } 167 | 168 | /** 169 | * Retrieves the defined routes. 170 | * 171 | * @return array The defined routes. 172 | */ 173 | public function getRoutes() 174 | { 175 | return $this->routes; 176 | } 177 | 178 | /** 179 | * Loads the routes from the Routes.php file. 180 | */ 181 | public function load() 182 | { 183 | $router = $this; 184 | require(dirname(__DIR__) . '/App/Routes.php'); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Core/View.php: -------------------------------------------------------------------------------- 1 | '../Cache', 39 | ]); 40 | } 41 | return $twig->render($file, $args); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Engine/Interface/ServiceProviderInterface.php: -------------------------------------------------------------------------------- 1 | di = $di; 26 | $this->user_agent = $user_agent ?? $this->di->get('request')->server['HTTP_USER_AGENT']; 27 | } 28 | 29 | /** 30 | * Get the user agent string. 31 | * 32 | * @return string The user agent string 33 | */ 34 | public function getUserAgentString(): string 35 | { 36 | return $this->user_agent; 37 | } 38 | 39 | /** 40 | * Check if the user agent represents a mobile device. 41 | * 42 | * @return bool True if the user agent is from a mobile device, false otherwise 43 | */ 44 | public function isMobile(): bool 45 | { 46 | return (bool) preg_match('/(android|iphone|ipad|windows phone)/i', $this->user_agent); 47 | } 48 | 49 | /** 50 | * Check if the user agent represents a tablet device. 51 | * 52 | * @return bool True if the user agent is from a tablet device, false otherwise 53 | */ 54 | public function isTablet(): bool 55 | { 56 | return (bool) preg_match('/(ipad)/i', $this->user_agent); 57 | } 58 | 59 | /** 60 | * Get the browser name from the user agent string. 61 | * 62 | * @return string The browser name 63 | */ 64 | public function getBrowser(): string 65 | { 66 | $browser = 'Unknown'; 67 | 68 | if (preg_match('/msie|trident/i', $this->user_agent)) { 69 | $browser = 'Internet Explorer'; 70 | } elseif (preg_match('/firefox/i', $this->user_agent)) { 71 | $browser = 'Firefox'; 72 | } elseif (preg_match('/chrome|crios/i', $this->user_agent)) { 73 | $browser = 'Chrome'; 74 | } elseif (preg_match('/safari/i', $this->user_agent)) { 75 | $browser = 'Safari'; 76 | } elseif (preg_match('/opera|opr/i', $this->user_agent)) { 77 | $browser = 'Opera'; 78 | } 79 | 80 | return $browser; 81 | } 82 | 83 | /** 84 | * Get the operating system name from the user agent string. 85 | * 86 | * @return string The operating system name 87 | */ 88 | public function getOperatingSystem(): string 89 | { 90 | $os = 'Unknown'; 91 | 92 | if (preg_match('/windows/i', $this->user_agent)) { 93 | $os = 'Windows'; 94 | } elseif (preg_match('/macintosh|mac os x/i', $this->user_agent)) { 95 | $os = 'Mac'; 96 | } elseif (preg_match('/android/i', $this->user_agent)) { 97 | $os = 'Android'; 98 | } elseif (preg_match('/iphone/i', $this->user_agent)) { 99 | $os = 'iPhone'; 100 | } elseif (preg_match('/ipad/i', $this->user_agent)) { 101 | $os = 'iPad'; 102 | } elseif (preg_match('/linux/i', $this->user_agent)) { 103 | $os = 'Linux'; 104 | } 105 | 106 | return $os; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Engine/cache.php: -------------------------------------------------------------------------------- 1 | adapter = new $class($di, \App\Config::cache_expire); 29 | } else { 30 | throw new \Exception("Error: cache engine not found"); 31 | } 32 | } 33 | 34 | /** 35 | * Get cached data by key. 36 | * 37 | * @param string $key The cache key 38 | * @return mixed|null The cached data or null if not found 39 | */ 40 | public function get(string $key) 41 | { 42 | return $this->adapter->get($key); 43 | } 44 | 45 | /** 46 | * Set cache data with key. 47 | * 48 | * @param string $key The cache key 49 | * @param mixed $data The data to be cached 50 | * @param int $expire Cache expiration time in seconds (optional) 51 | * @return void 52 | */ 53 | public function set(string $key, $data, int $expire = \App\Config::cache_expire): void 54 | { 55 | $this->adapter->set($key, $data, $expire); 56 | } 57 | 58 | /** 59 | * Delete cached data by key. 60 | * 61 | * @param string $key The cache key 62 | * @return void 63 | */ 64 | public function delete(string $key): void 65 | { 66 | $this->adapter->delete($key); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Engine/config.php: -------------------------------------------------------------------------------- 1 | directory = $path; 33 | } 34 | 35 | /** 36 | * Get configuration value by key. 37 | * 38 | * @param string $key The configuration key 39 | * @return mixed The configuration value 40 | */ 41 | public function get(string $key): mixed 42 | { 43 | return isset($this->data[$key]) ? $this->data[$key] : ''; 44 | } 45 | 46 | /** 47 | * Set configuration value by key. 48 | * 49 | * @param string $key The configuration key 50 | * @param mixed $data The configuration value 51 | * @return void 52 | */ 53 | public function set(string $key, mixed $data): void 54 | { 55 | $this->data[$key] = $data; 56 | } 57 | 58 | /** 59 | * Check if configuration key exists. 60 | * 61 | * @param string $key The configuration key 62 | * @return bool True if key exists, false otherwise 63 | */ 64 | public function has(string $key): bool 65 | { 66 | return isset($this->data[$key]); 67 | } 68 | 69 | /** 70 | * Load configuration from a file. 71 | * 72 | * @param string $filename The configuration file name 73 | * @return array The loaded configuration data 74 | */ 75 | public function load(string $filename): array 76 | { 77 | $file = $this->directory . $filename . '.php'; 78 | $namespace = ''; 79 | 80 | if (is_file($file)) { 81 | $_config = []; 82 | require $file; 83 | $this->data = array_merge($this->data, $_config); 84 | return $this->data; 85 | } 86 | return []; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Engine/db.php: -------------------------------------------------------------------------------- 1 | adapter = DbConnection::getDb($db_engine, $hostname, $username, $password, $dbname, $port); 39 | } 40 | 41 | /** 42 | * Execute a database query. 43 | * 44 | * @param string $query The SQL query to execute. 45 | * @param array $params The query parameters. 46 | * 47 | * @return mixed The query result. 48 | */ 49 | public function runQuery(string $query, $params = []): mixed 50 | { 51 | return $this->adapter->runQuery($query, $params); 52 | } 53 | 54 | /** 55 | * Get the number of rows affected by the last query. 56 | * 57 | * @return int The number of affected rows. 58 | */ 59 | public function countAffected(): int 60 | { 61 | return $this->adapter->countAffected(); 62 | } 63 | 64 | /** 65 | * Get the last inserted ID. 66 | * 67 | * @return int The last inserted ID. 68 | */ 69 | public function getLastInsertId(): int 70 | { 71 | return $this->adapter->getLastInsertId(); 72 | } 73 | 74 | /** 75 | * Check if the database connection is active. 76 | * 77 | * @return bool True if connected, false otherwise. 78 | */ 79 | public function isConnected(): bool 80 | { 81 | return $this->adapter->isConnected(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Engine/di.php: -------------------------------------------------------------------------------- 1 | get($key); 21 | } 22 | 23 | /** 24 | * Magic method to set a dependency by key. 25 | * 26 | * @param string $key The key of the dependency 27 | * @param object $value The value of the dependency 28 | */ 29 | public function __set(string $key, object $value): void 30 | { 31 | $this->set($key, $value); 32 | } 33 | 34 | /** 35 | * Set a dependency by key. 36 | * 37 | * @param string $key The key of the dependency 38 | * @param object $value The value of the dependency 39 | */ 40 | public function set(string $key, object $value): void 41 | { 42 | $this->data[$key] = $value; 43 | } 44 | 45 | /** 46 | * Get a dependency by key. 47 | * 48 | * @param string $key The key of the dependency 49 | * @return mixed|null The dependency value if found, null otherwise 50 | */ 51 | public function get(string $key) 52 | { 53 | return isset($this->data[$key]) ? $this->data[$key] : null; 54 | } 55 | 56 | /** 57 | * Check if a dependency exists by key. 58 | * 59 | * @param string $key The key of the dependency 60 | * @return bool True if the dependency exists, false otherwise 61 | */ 62 | public function has(string $key): bool 63 | { 64 | return isset($this->data[$key]); 65 | } 66 | 67 | /** 68 | * Remove a dependency by key. 69 | * 70 | * @param string $key The key of the dependency 71 | */ 72 | public function remove(string $key): void 73 | { 74 | if (isset($this->data[$key])) { 75 | unset($this->data[$key]); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Engine/file.php: -------------------------------------------------------------------------------- 1 | di = $di; 30 | self::$file = $this->di->get('request')->files; 31 | } 32 | 33 | /** 34 | * Handle file upload. 35 | * 36 | * @param string $fileInputName The name of the file input field 37 | * @param string $destination The destination directory to save the uploaded file 38 | * @param array|null $allowTypes An array of allowed file extensions 39 | * @param int|null $maxSize The maximum allowed file size in bytes 40 | * @param bool $sanitizeName Whether to sanitize the file name 41 | * @param string|null $newFilename The new file name (optional) 42 | * @return bool|string Returns the destination path if the upload is successful, false otherwise 43 | */ 44 | public static function handleUpload( 45 | string $fileInputName, 46 | string $destination, 47 | array $allowTypes = null, 48 | int $maxSize = null, 49 | bool $sanitizeName = true, 50 | ?string $newFilename = null 51 | ): bool|string { 52 | if (!isset(static::$file[$fileInputName])) { 53 | return false; 54 | } 55 | $file = static::$file[$fileInputName]; 56 | 57 | if ($file['error'] !== UPLOAD_ERR_OK) { 58 | return false; 59 | } 60 | 61 | $fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); 62 | 63 | $maxSize = $maxSize ?? \App\Config::file_upload_max_size; 64 | 65 | $allowTypes = $allowTypes ?? \App\Config::file_upload_allow_type; 66 | 67 | if (!in_array($fileExtension, $allowTypes)) { 68 | return false; 69 | } 70 | 71 | if ($file['size'] > $maxSize) { 72 | return false; 73 | } 74 | 75 | if ($newFilename !== null) { 76 | $filename = $newFilename; 77 | } else { 78 | $filename = $sanitizeName ? self::sanitizeFileName($file['name']) : $file['name']; 79 | } 80 | 81 | $destinationPath = rtrim($destination, '/') . '/' . $filename; 82 | $counter = 1; 83 | 84 | while (file_exists($destinationPath)) { 85 | $filename = self::generateUniqueFilename($fileExtension, $counter); 86 | $destinationPath = rtrim($destination, '/') . '/' . $filename; 87 | $counter++; 88 | } 89 | 90 | if (move_uploaded_file($file['tmp_name'], $destinationPath)) { 91 | return $destinationPath; 92 | } 93 | 94 | return false; 95 | } 96 | 97 | /** 98 | * Generate a unique filename with a counter. 99 | * 100 | * @param string $fileExtension The file extension 101 | * @param int $counter The counter value 102 | * @return string The generated unique filename 103 | */ 104 | protected static function generateUniqueFilename(string $fileExtension, int $counter): string 105 | { 106 | $filename = 'file_' . $counter . '.' . $fileExtension; 107 | 108 | return $filename; 109 | } 110 | 111 | /** 112 | * Sanitize the file name by removing unwanted characters. 113 | * 114 | * @param string $filename The original file name 115 | * @return string The sanitized file name 116 | */ 117 | protected static function sanitizeFileName(string $filename): string 118 | { 119 | $filename = preg_replace('/[^\w\s.-]/', '', $filename); 120 | $filename = str_replace(' ', '_', $filename); 121 | return $filename; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Engine/hook.php: -------------------------------------------------------------------------------- 1 | di = $di; 21 | } 22 | 23 | /** 24 | * Add a custom hook with the specified key, hook function, and options. 25 | * 26 | * @param string $key The key to identify the hook 27 | * @param callable $hook The hook function to execute 28 | * @param array $options Additional options for the hook 29 | */ 30 | public static function addCustomHook(string $key, callable $hook, array $options = []) 31 | { 32 | self::addHook($key, $hook, $options); 33 | } 34 | 35 | /** 36 | * Add a hook with the specified key, hook function, and options. 37 | * 38 | * @param string $key The key to identify the hook 39 | * @param callable $hook The hook function to execute 40 | * @param array $options Additional options for the hook 41 | */ 42 | public static function addHook(string $key, callable $hook, array $options = ["priority" => 0]) 43 | { 44 | // Modify the key if 'runwhen' option is provided 45 | if (!empty($options['runwhen'] ?? false)) { 46 | $key = $options['runwhen'] . ':' . $key; 47 | } 48 | 49 | // Initialize the hooks array for the key if it doesn't exist 50 | if (!isset(self::$hooks[$key])) { 51 | self::$hooks[$key] = []; 52 | } 53 | 54 | // Add the hook to the hooks array with priority 55 | self::$hooks[$key][] = [ 56 | 'hook' => $hook, 57 | 'priority' => isset($options["priority"]) ? $options['priority'] : 0 58 | ]; 59 | 60 | // Sort the hooks based on priority 61 | $hooks = &self::$hooks[$key]; 62 | array_multisort(array_column($hooks, 'priority'), SORT_DESC, array_keys($hooks), SORT_ASC, $hooks); 63 | } 64 | 65 | /** 66 | * Get the hooks registered for the specified key. 67 | * 68 | * @param string $key The key identifying the hooks 69 | * @return array|null Array of hooks or null if no hooks found 70 | */ 71 | public static function getHook(string $key): ?array 72 | { 73 | return self::hasHook($key) ? self::$hooks[$key] : null; 74 | } 75 | 76 | /** 77 | * Check if hooks are registered for the specified key. 78 | * 79 | * @param string $key The key identifying the hooks 80 | * @return bool True if hooks are registered, false otherwise 81 | */ 82 | public static function hasHook(string $key): bool 83 | { 84 | return isset(self::$hooks[$key]); 85 | } 86 | 87 | /** 88 | * Load the hook files from the specified directory. 89 | */ 90 | public function load(): void 91 | { 92 | $hook = $this; 93 | 94 | // Load hook files from the directory 95 | foreach (glob(dirname(__DIR__) . '/App/Hook/*.php') as $filename) { 96 | require_once $filename; 97 | } 98 | // Example: 99 | // require(dirname(__DIR__) . '/App/Hook/BeforeControllerHook.php'); 100 | // require(dirname(__DIR__) . '/App/Hook/AfterControllerHook.php'); 101 | } 102 | 103 | /** 104 | * Execute the hooks registered for the specified key. 105 | * 106 | * @param string $key The key identifying the hooks to execute 107 | */ 108 | public function execute(string $key): void 109 | { 110 | if (isset(self::$hooks[$key])) { 111 | foreach (self::$hooks[$key] as $hook) { 112 | $hook['hook']($this->di); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Engine/language.php: -------------------------------------------------------------------------------- 1 | directory = $directory; 30 | $this->di = $di; 31 | $this->code = !empty($code) ? $code : $this->findLanguage(); 32 | } 33 | 34 | /** 35 | * Determines the language code based on the request data. 36 | * 37 | * @return string The language code 38 | */ 39 | private function findLanguage(): string 40 | { 41 | $request = $this->di->get('request'); 42 | $code = $request->get['language'] ?? $request->cookie['language'] ?? $request->server['HTTP_ACCEPT_LANGUAGE']; 43 | $code = $this->sanitizeLanguageCode($code); 44 | 45 | return $code; 46 | } 47 | 48 | /** 49 | * Sanitizes a language code, ensuring it is in a valid format and supported. 50 | * 51 | * @param string $language The language code to sanitize 52 | * @return string The sanitized language code 53 | */ 54 | private function sanitizeLanguageCode(string $language): string 55 | { 56 | $language = strtolower($language); 57 | $language = preg_replace('/[^a-z\-]/', '', $language); 58 | $supported_languages = \App\Config::support_languages; 59 | if (!in_array($language, $supported_languages)) { 60 | $language = 'en'; 61 | } 62 | return $language; 63 | } 64 | 65 | /** 66 | * Retrieves a translation value for the specified key. 67 | * 68 | * @param string $key The translation key 69 | * @return mixed|null The translation value, or null if not found 70 | */ 71 | public function get(string $key) 72 | { 73 | return $this->data[$key] ?? null; 74 | } 75 | 76 | /** 77 | * Sets a translation value for the specified key. 78 | * 79 | * @param string $key The translation key 80 | * @param mixed $data The translation value 81 | */ 82 | public function set(string $key, $data) 83 | { 84 | $this->data[$key] = $data; 85 | } 86 | 87 | /** 88 | * Loads the translation file for the specified language and merges the translations with the existing data. 89 | * 90 | * @param string $filename The filename of the translation file 91 | * @param string $code The language code (optional, defaults to the current language code) 92 | * @return array The merged translation data 93 | * @throws \Exception If the language file is not found 94 | */ 95 | public function load(string $filename, string $code = ''): array 96 | { 97 | $code = $code ?: $this->code; 98 | 99 | if (!isset($this->cache[$code][$filename])) { 100 | $_ = []; 101 | $file = realpath($this->directory . $code . '/' . $filename . '.php'); 102 | 103 | if ($file && is_file($file)) { 104 | require($file); 105 | $this->cache[$code][$filename] = $_; 106 | } else { 107 | throw new \Exception('Language file not found: ' . $filename); 108 | } 109 | } else { 110 | $_ = $this->cache[$code][$filename]; 111 | } 112 | $this->data = array_merge($this->data, $_); 113 | 114 | return $this->data; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Engine/log.php: -------------------------------------------------------------------------------- 1 | log_path = $log_path; 17 | } 18 | 19 | /** 20 | * Write a log message to the log file. 21 | * 22 | * @param string $message The log message to be written. 23 | * 24 | * @return void 25 | */ 26 | public function logging(string $message): void 27 | { 28 | $log = "[" . date("Y-m-d H:i:s") . "] " . $message . PHP_EOL; 29 | file_put_contents($this->log_path, $log, FILE_APPEND | LOCK_EX); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Engine/profile.php: -------------------------------------------------------------------------------- 1 | di = $di; 20 | } 21 | 22 | /** 23 | * Start profiling. 24 | * 25 | * @return void 26 | */ 27 | public function start(): void 28 | { 29 | $this->start_time = microtime(true); 30 | $this->mem_start_usage = memory_get_usage(true); 31 | $this->addFunctionCall(__METHOD__); 32 | } 33 | 34 | /** 35 | * End profiling and print the results. 36 | * 37 | * @return void 38 | */ 39 | public function end(): void 40 | { 41 | $execution_time = microtime(true) - $this->start_time; 42 | $memory_usage = memory_get_peak_usage(true) - $this->mem_start_usage; 43 | $this->addFunctionCall(__METHOD__); 44 | 45 | $this->printProfile($execution_time, $memory_usage); 46 | } 47 | 48 | /** 49 | * Add a function call to the list of profiled function calls. 50 | * 51 | * @param string $functionName The name of the function being called. 52 | * 53 | * @return void 54 | */ 55 | public function addFunctionCall($functionName): void 56 | { 57 | $this->functions_call[] = $functionName; 58 | } 59 | 60 | /** 61 | * Print the profiling results. 62 | * 63 | * @param float $execution_time The execution time in seconds. 64 | * @param int $memory_usage The memory usage in bytes. 65 | * 66 | * @return void 67 | */ 68 | private function printProfile($execution_time, $memory_usage): void 69 | { 70 | echo "Execution Time: {$execution_time} seconds" . PHP_EOL; 71 | echo "Memory Usage: {$memory_usage} bytes" . PHP_EOL; 72 | echo "Function calls:" . PHP_EOL; 73 | foreach ($this->functions_call as $function_call) { 74 | echo $function_call . PHP_EOL; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Engine/request.php: -------------------------------------------------------------------------------- 1 | get = $this->clear($_GET); 19 | $this->post = $this->clear($_POST); 20 | $this->files = $this->clear($_FILES); 21 | $this->server = $this->clear($_SERVER); 22 | $this->cookie = $this->clear($_COOKIE); 23 | } 24 | 25 | /** 26 | * Clear the input data by removing potential security risks and sanitizing the values. 27 | * 28 | * @param mixed $data The input data to be cleared. 29 | * 30 | * @return mixed The cleared input data. 31 | */ 32 | private function clear($data): mixed 33 | { 34 | if (is_array($data)) { 35 | foreach ($data as $key => $val) { 36 | unset($data[$key]); 37 | $data[$this->clear($key)] = $this->clear($val); 38 | } 39 | } else { 40 | $data = trim(htmlspecialchars($data, ENT_COMPAT, 'UTF-8')); 41 | } 42 | 43 | return $data; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Engine/response.php: -------------------------------------------------------------------------------- 1 | di = $di; 21 | } 22 | 23 | /** 24 | * Add a header to the response. 25 | * 26 | * @param string $header The header string to add. 27 | * 28 | * @return self 29 | */ 30 | public function addHeader(string $header): self 31 | { 32 | $this->headers[] = $header; 33 | return $this; 34 | } 35 | 36 | /** 37 | * Get the headers of the response. 38 | * 39 | * @return array The response headers. 40 | */ 41 | public function getHeader(): array 42 | { 43 | return $this->headers; 44 | } 45 | 46 | /** 47 | * Get the output of the response. 48 | * 49 | * @return mixed The response output. 50 | */ 51 | public function getOutput() 52 | { 53 | return $this->output; 54 | } 55 | 56 | /** 57 | * Compress the response data based on the compression level and encoding. 58 | * 59 | * @param int $level The compression level. 60 | * @param string|array $data The data to compress. 61 | * 62 | * @return string The compressed data. 63 | */ 64 | private function compress(int $level, $data): string 65 | { 66 | $level = $level ?: $this->compressLevel; 67 | $data = is_array($data) ? json_encode($data) : $data; 68 | if ($level < -1 || $level > 9) { 69 | return $data; 70 | } 71 | if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) { 72 | $this->encoding = 'gzip'; 73 | } elseif (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== false) { 74 | $this->encoding = 'x-gzip'; 75 | } 76 | 77 | if (!isset($this->encoding)) { 78 | return $data; 79 | } 80 | 81 | if (!extension_loaded('zlib') || ini_get('zlib.output_compression')) { 82 | return $data; 83 | } 84 | 85 | if (headers_sent() || connection_status()) { 86 | return $data; 87 | } 88 | // $compressed = gzencode($data, $level); 89 | // $this->addHeader('Content-Encoding: ' . $this->encoding); 90 | // $this->addHeader('Content-Length: ' . strlen($compressed)); 91 | 92 | // return $compressed; 93 | return $data; 94 | } 95 | 96 | /** 97 | * Set the compression level for the response. 98 | * 99 | * @param int $compressLevel The compression level to set. 100 | */ 101 | public function setCompressionLevel(int $compressLevel): void 102 | { 103 | $this->compressLevel = $compressLevel; 104 | } 105 | 106 | /** 107 | * Set the output of the response. 108 | * 109 | * @param mixed $output The response output. 110 | * 111 | * @return self 112 | */ 113 | public function setOutput($output): self 114 | { 115 | $this->output = $output; 116 | return $this; 117 | } 118 | 119 | public function output(): void 120 | { 121 | if ($this->output) { 122 | $this->output = $this->compressLevel ? $this->compress($this->compressLevel, $this->output) : $this->output; 123 | } 124 | $current_route = $this->currentRoute(); 125 | if (!headers_sent()) { 126 | foreach ($this->headers as $header) { 127 | header($header, true); 128 | } 129 | } 130 | echo $this->output; 131 | $this->afterActionHook($current_route); 132 | $this->afterControllerHook($current_route); 133 | // After action hook 134 | } 135 | 136 | /** 137 | * Get the current route object from the dependency injection container. 138 | * 139 | * @return mixed The current route object. 140 | */ 141 | private function currentRoute() 142 | { 143 | return $this->di->get('currentRoute'); 144 | } 145 | 146 | /** 147 | * Call the afterAction method on the current route object. 148 | * 149 | * @param mixed $current_object The current route object. 150 | */ 151 | public function afterActionHook($current_object): void 152 | { 153 | 154 | $current_object->afterAction(); 155 | } 156 | 157 | /** 158 | * Call the afterController method on the current route object. 159 | * 160 | * @param mixed $current_object The current route object. 161 | */ 162 | public function afterControllerHook($current_object): void 163 | { 164 | $current_object->afterController(); 165 | } 166 | 167 | public function redirect($url) 168 | { 169 | header('Location: ' . $url, true, 302); 170 | exit(); 171 | } 172 | 173 | public function json(array $responseData) 174 | { 175 | $this->addHeader('Content-Type: application/json'); 176 | 177 | // Convert the data to JSON format 178 | $jsonData = json_encode($responseData); 179 | 180 | // Output the JSON response 181 | echo $jsonData; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Engine/session.php: -------------------------------------------------------------------------------- 1 | adapter = new $session_engine($di); 24 | 25 | register_shutdown_function([&$this, 'close']); 26 | register_shutdown_function([&$this, 'gc']); 27 | } else { 28 | throw new \Exception('Error: Could not load session adapter ' . $session_engine . ' session!'); 29 | } 30 | } 31 | 32 | /** 33 | * Get the session ID. 34 | * 35 | * @return string|null The session ID. 36 | */ 37 | public function getId(): ?string 38 | { 39 | return $this->session_id; 40 | } 41 | 42 | /** 43 | * Start the session. 44 | * 45 | * @param string|null $session_id The session ID to start. If null, a new session ID will be generated. 46 | * 47 | * @return string|null The started session ID. 48 | * 49 | * @throws \Exception If an invalid session ID is provided. 50 | */ 51 | public function start(?string $session_id = null): ?string 52 | { 53 | // Prevent the start function from being called multiple times 54 | if (session_status() !== PHP_SESSION_ACTIVE) { 55 | // Set the session name 56 | session_name(\App\Config::session_name); 57 | if ($session_id !== null) { 58 | session_id($session_id); 59 | } 60 | // Start the session 61 | session_start(); 62 | 63 | if ($session_id === null) { 64 | $session_id = session_id(); 65 | } 66 | 67 | // Check if the session ID is valid 68 | if (!preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $session_id)) { 69 | throw new \Exception('Error: Invalid session ID!'); 70 | } 71 | 72 | $this->session_id = $session_id; 73 | } 74 | 75 | $this->data = $this->adapter->read($session_id); 76 | 77 | return $session_id; 78 | } 79 | 80 | /** 81 | * Close the session. 82 | */ 83 | public function close(): void 84 | { 85 | if (session_status() === PHP_SESSION_ACTIVE) { 86 | $this->adapter->write($this->session_id, $this->data); 87 | session_write_close(); 88 | } 89 | } 90 | 91 | /** 92 | * Perform garbage collection on the session. 93 | */ 94 | public function gc(): void 95 | { 96 | $this->adapter->gc($this->session_id); 97 | } 98 | 99 | /** 100 | * Destroy the session. 101 | */ 102 | public function destroy(): void 103 | { 104 | $this->data = []; 105 | $this->adapter->destroy($this->session_id); 106 | 107 | // Clear the session data 108 | session_unset(); 109 | 110 | // Destroy the session 111 | session_destroy(); 112 | 113 | $this->session_id = null; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Engine/theme.php: -------------------------------------------------------------------------------- 1 | theme = $theme; 19 | $this->di = $di; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CannonPHP MVC Framework 2 | 3 | [](https://opensource.org/licenses/MIT) 4 | 5 | Cannon MVC is an exceptional web application framework that embraces an expressive and elegant syntax. We strongly believe that development should be an enjoyable and creative experience, leading to true fulfillment. With Cannon MVC, you can bid farewell to the cumbersome aspects of development by effortlessly handling common tasks encountered in numerous web projects. Some of the remarkable benefits and features of Cannon MVC include: 6 | 7 | ## Features 8 | 9 | - **Routing**: The framework offers a powerful routing system that effortlessly maps incoming requests to controller actions. 10 | - **Di container**: Simplifies the management of dependencies, promoting loose coupling and flexible object creation and handling. 11 | - **session and cache storage**: Provides seamless integration with multiple session and cache drivers for efficient data storage. 12 | - **Tempalte engine**: Supports the popular Twig template engine out of the box, or allows the use of your preferred choice. 13 | - **Hooks**: Enables the execution of custom code before and after controller actions for enhanced customization. 14 | - **Database ORM**: Includes an intuitive and feature-rich database ORM that simplifies database operations within the framework. 15 | 16 | ## Requirements 17 | 18 | - PHP version 8 or higher 19 | - Composer (https://getcomposer.org) for dependency management 20 | 21 | ## Installation 22 | 23 | 1. create a new CannonPHP application using composer’s create-project command: 24 | 25 | ```bash 26 | composer create-project --prefer-dist cannonphp/app 27 | ``` 28 | 29 | 2. `cd app`, run `composer install` to install the required dependencies. 30 | 3. Configure your web server to point to the public directory as the document root. 31 | 4. Customize the framework's `Config.php` file located in the `App` directory, such as database settings, routes, etc. 32 | 5. Start building your application by creating controllers, models, and views in their respective directories. 33 | 34 | ## Usage 35 | 36 | 1. Define your application routes in the `routes.php` file located in the `App` directory. 37 | 2. Create controllers in the `App/Controller` directory to handle different actions. 38 | 3. Define models in the `App/Model` directory to interact with the database. 39 | 4. Create views in the `App/View` directory to render the presentation layer. 40 | 5. Customize the `BaseController` class according to your application's needs if necessary. 41 | 6. Extends the framework by adding your own service to the framework using service provider if necessary. 42 | 43 | ## Contributing 44 | 45 | Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request. 46 | 47 | ## License 48 | 49 | This MVC framework is open-source software licensed under the [MIT License](https://opensource.org/licenses/MIT). 50 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO List 2 | 3 | - [ ] Implement user authentication system. 4 | - [ ] Implement RBAC functionality sysmte. 5 | - [ ] Write unit tests for critical components. 6 | - [ ] Change to use .env file to load credentials instead of using config file. 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cannonphp/app", 3 | "description": "Cannon MVC is an exceptional web application framework that embraces an expressive and elegant syntax. ", 4 | "config": { 5 | "sort-packages":true 6 | }, 7 | "require": { 8 | "twig/twig": "^3.0" 9 | }, 10 | "autoload": { 11 | "psr-4": { 12 | "Core\\": "Core/", 13 | "Engine\\": "Engine/", 14 | "App\\": "App/" 15 | } 16 | }, 17 | "require-dev": { 18 | "phpstan/phpstan": "^1.10" 19 | }, 20 | "scripts": { 21 | "phpstan": "vendor/bin/phpstan analyze -c phpstan.neon" 22 | }, 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "30f03dd02378a66545c449a779783af7", 8 | "packages": [ 9 | { 10 | "name": "symfony/polyfill-ctype", 11 | "version": "v1.27.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/symfony/polyfill-ctype.git", 15 | "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", 20 | "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.1" 25 | }, 26 | "provide": { 27 | "ext-ctype": "*" 28 | }, 29 | "suggest": { 30 | "ext-ctype": "For best performance" 31 | }, 32 | "type": "library", 33 | "extra": { 34 | "branch-alias": { 35 | "dev-main": "1.27-dev" 36 | }, 37 | "thanks": { 38 | "name": "symfony/polyfill", 39 | "url": "https://github.com/symfony/polyfill" 40 | } 41 | }, 42 | "autoload": { 43 | "files": [ 44 | "bootstrap.php" 45 | ], 46 | "psr-4": { 47 | "Symfony\\Polyfill\\Ctype\\": "" 48 | } 49 | }, 50 | "notification-url": "https://packagist.org/downloads/", 51 | "license": [ 52 | "MIT" 53 | ], 54 | "authors": [ 55 | { 56 | "name": "Gert de Pagter", 57 | "email": "BackEndTea@gmail.com" 58 | }, 59 | { 60 | "name": "Symfony Community", 61 | "homepage": "https://symfony.com/contributors" 62 | } 63 | ], 64 | "description": "Symfony polyfill for ctype functions", 65 | "homepage": "https://symfony.com", 66 | "keywords": [ 67 | "compatibility", 68 | "ctype", 69 | "polyfill", 70 | "portable" 71 | ], 72 | "support": { 73 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" 74 | }, 75 | "funding": [ 76 | { 77 | "url": "https://symfony.com/sponsor", 78 | "type": "custom" 79 | }, 80 | { 81 | "url": "https://github.com/fabpot", 82 | "type": "github" 83 | }, 84 | { 85 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 86 | "type": "tidelift" 87 | } 88 | ], 89 | "time": "2022-11-03T14:55:06+00:00" 90 | }, 91 | { 92 | "name": "symfony/polyfill-mbstring", 93 | "version": "v1.27.0", 94 | "source": { 95 | "type": "git", 96 | "url": "https://github.com/symfony/polyfill-mbstring.git", 97 | "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" 98 | }, 99 | "dist": { 100 | "type": "zip", 101 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", 102 | "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", 103 | "shasum": "" 104 | }, 105 | "require": { 106 | "php": ">=7.1" 107 | }, 108 | "provide": { 109 | "ext-mbstring": "*" 110 | }, 111 | "suggest": { 112 | "ext-mbstring": "For best performance" 113 | }, 114 | "type": "library", 115 | "extra": { 116 | "branch-alias": { 117 | "dev-main": "1.27-dev" 118 | }, 119 | "thanks": { 120 | "name": "symfony/polyfill", 121 | "url": "https://github.com/symfony/polyfill" 122 | } 123 | }, 124 | "autoload": { 125 | "files": [ 126 | "bootstrap.php" 127 | ], 128 | "psr-4": { 129 | "Symfony\\Polyfill\\Mbstring\\": "" 130 | } 131 | }, 132 | "notification-url": "https://packagist.org/downloads/", 133 | "license": [ 134 | "MIT" 135 | ], 136 | "authors": [ 137 | { 138 | "name": "Nicolas Grekas", 139 | "email": "p@tchwork.com" 140 | }, 141 | { 142 | "name": "Symfony Community", 143 | "homepage": "https://symfony.com/contributors" 144 | } 145 | ], 146 | "description": "Symfony polyfill for the Mbstring extension", 147 | "homepage": "https://symfony.com", 148 | "keywords": [ 149 | "compatibility", 150 | "mbstring", 151 | "polyfill", 152 | "portable", 153 | "shim" 154 | ], 155 | "support": { 156 | "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" 157 | }, 158 | "funding": [ 159 | { 160 | "url": "https://symfony.com/sponsor", 161 | "type": "custom" 162 | }, 163 | { 164 | "url": "https://github.com/fabpot", 165 | "type": "github" 166 | }, 167 | { 168 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 169 | "type": "tidelift" 170 | } 171 | ], 172 | "time": "2022-11-03T14:55:06+00:00" 173 | }, 174 | { 175 | "name": "twig/twig", 176 | "version": "v3.6.1", 177 | "source": { 178 | "type": "git", 179 | "url": "https://github.com/twigphp/Twig.git", 180 | "reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd" 181 | }, 182 | "dist": { 183 | "type": "zip", 184 | "url": "https://api.github.com/repos/twigphp/Twig/zipball/7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd", 185 | "reference": "7e7d5839d4bec168dfeef0ac66d5c5a2edbabffd", 186 | "shasum": "" 187 | }, 188 | "require": { 189 | "php": ">=7.2.5", 190 | "symfony/polyfill-ctype": "^1.8", 191 | "symfony/polyfill-mbstring": "^1.3" 192 | }, 193 | "require-dev": { 194 | "psr/container": "^1.0|^2.0", 195 | "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" 196 | }, 197 | "type": "library", 198 | "autoload": { 199 | "psr-4": { 200 | "Twig\\": "src/" 201 | } 202 | }, 203 | "notification-url": "https://packagist.org/downloads/", 204 | "license": [ 205 | "BSD-3-Clause" 206 | ], 207 | "authors": [ 208 | { 209 | "name": "Fabien Potencier", 210 | "email": "fabien@symfony.com", 211 | "homepage": "http://fabien.potencier.org", 212 | "role": "Lead Developer" 213 | }, 214 | { 215 | "name": "Twig Team", 216 | "role": "Contributors" 217 | }, 218 | { 219 | "name": "Armin Ronacher", 220 | "email": "armin.ronacher@active-4.com", 221 | "role": "Project Founder" 222 | } 223 | ], 224 | "description": "Twig, the flexible, fast, and secure template language for PHP", 225 | "homepage": "https://twig.symfony.com", 226 | "keywords": [ 227 | "templating" 228 | ], 229 | "support": { 230 | "issues": "https://github.com/twigphp/Twig/issues", 231 | "source": "https://github.com/twigphp/Twig/tree/v3.6.1" 232 | }, 233 | "funding": [ 234 | { 235 | "url": "https://github.com/fabpot", 236 | "type": "github" 237 | }, 238 | { 239 | "url": "https://tidelift.com/funding/github/packagist/twig/twig", 240 | "type": "tidelift" 241 | } 242 | ], 243 | "time": "2023-06-08T12:52:13+00:00" 244 | } 245 | ], 246 | "packages-dev": [ 247 | { 248 | "name": "phpstan/phpstan", 249 | "version": "1.10.21", 250 | "source": { 251 | "type": "git", 252 | "url": "https://github.com/phpstan/phpstan.git", 253 | "reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5" 254 | }, 255 | "dist": { 256 | "type": "zip", 257 | "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5", 258 | "reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5", 259 | "shasum": "" 260 | }, 261 | "require": { 262 | "php": "^7.2|^8.0" 263 | }, 264 | "conflict": { 265 | "phpstan/phpstan-shim": "*" 266 | }, 267 | "bin": [ 268 | "phpstan", 269 | "phpstan.phar" 270 | ], 271 | "type": "library", 272 | "autoload": { 273 | "files": [ 274 | "bootstrap.php" 275 | ] 276 | }, 277 | "notification-url": "https://packagist.org/downloads/", 278 | "license": [ 279 | "MIT" 280 | ], 281 | "description": "PHPStan - PHP Static Analysis Tool", 282 | "keywords": [ 283 | "dev", 284 | "static analysis" 285 | ], 286 | "support": { 287 | "docs": "https://phpstan.org/user-guide/getting-started", 288 | "forum": "https://github.com/phpstan/phpstan/discussions", 289 | "issues": "https://github.com/phpstan/phpstan/issues", 290 | "security": "https://github.com/phpstan/phpstan/security/policy", 291 | "source": "https://github.com/phpstan/phpstan-src" 292 | }, 293 | "funding": [ 294 | { 295 | "url": "https://github.com/ondrejmirtes", 296 | "type": "github" 297 | }, 298 | { 299 | "url": "https://github.com/phpstan", 300 | "type": "github" 301 | }, 302 | { 303 | "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", 304 | "type": "tidelift" 305 | } 306 | ], 307 | "time": "2023-06-21T20:07:58+00:00" 308 | } 309 | ], 310 | "aliases": [], 311 | "minimum-stability": "stable", 312 | "stability-flags": [], 313 | "prefer-stable": false, 314 | "prefer-lowest": false, 315 | "platform": [], 316 | "platform-dev": [], 317 | "plugin-api-version": "2.3.0" 318 | } 319 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cannonmaster/CannonPHP/6a46aefaaf8c2f690599a05718c6b45dafdbb479/log/.gitkeep -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 3 3 | paths: 4 | - Engine 5 | - Core -------------------------------------------------------------------------------- /phpstan.neon.list: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan.neon -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteBase / 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteRule ^(.*)$ index.php?$1 [L,QSA] -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | load(); 18 | 19 | use App\Config; 20 | use Core\Router; 21 | use Engine\Di; 22 | use Engine\Error; 23 | use Engine\Hook; 24 | use Engine\Request; 25 | use Engine\Db; 26 | use Engine\Session; 27 | use Engine\Cache; 28 | use Engine\Language; 29 | use Engine\Response; 30 | use Engine\Interface; 31 | 32 | define('LOG_FOLDER', dirname(__DIR__) . '/log/'); 33 | define('CACHE_FOLDER', dirname(__DIR__) . '/Storage/cache/'); 34 | // error handler 35 | error_reporting(E_ALL); 36 | 37 | // config error handler, and setup the logging system 38 | // the logging system use the /log folder to log. 39 | set_error_handler('Core\Error::errorHandler'); 40 | set_exception_handler('Core\Error::exceptionHandler'); 41 | 42 | // di 43 | $di = new Engine\Di(); 44 | // global config file 45 | $config = new App\Config(); 46 | $di->set('config', $config); 47 | 48 | $log = new Engine\Log(App\Config::log_path); 49 | // $log->logging('abc'); 50 | $di->set('log', $log); 51 | 52 | // routing system 53 | $router = new Core\Router($di); 54 | // routes registers 55 | $router->load(); 56 | $di->set('router', $router); 57 | 58 | 59 | // sanitize the global super variable such as $_GET, $_POST, $_FILES , etc ... 60 | $request = new Engine\Request(); 61 | $di->set('request', $request); 62 | 63 | // setup database 64 | 65 | // connection tested but not the query, todo: test query etc ... 66 | if (App\Config::db_autostart) { 67 | $db = new Engine\Db($di); 68 | $di->set('db', $db); 69 | } 70 | 71 | 72 | // enable session if user config the serssion auto start "true" 73 | if (App\Config::session_autostart) { 74 | 75 | $session = new Engine\Session(App\Config::session_engine, $di); 76 | $di->set('session', $session); 77 | 78 | if (isset($request->cookie[App\Config::session_name])) { 79 | $session_id = $request->cookie[App\Config::session_name]; 80 | } else { 81 | $session_id = ''; 82 | } 83 | 84 | // start the session will retrive the data from the session engine, the session engine could be redis or db 85 | // todo: add file as session data storage 86 | $session->start($session_id); 87 | $option = [ 88 | 'expires' => 0, 89 | 'path' => App\Config::session_path, 90 | 'domain' => App\Config::session_domain, 91 | 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', 92 | 'httponly' => App\Config::session_http_only, 93 | 'Samesite' => App\Config::session_samesite 94 | ]; 95 | 96 | setcookie(App\Config::session_name, $session->getId(), $option); 97 | } 98 | 99 | // cache system (need to be test) 100 | $cache = new Engine\Cache(App\Config::cache_engine, $di); 101 | $di->set('cache', $cache); 102 | 103 | // language system (need to be test) 104 | $language = new Engine\Language(App\Config::language, dirname(__DIR__) . '/App/Language/', $di); 105 | setcookie('language', $language->code, time() + 3600 * 24 * 30, '/'); 106 | $di->set('language', $language); 107 | 108 | $theme = new Engine\Theme(App\Config::theme, $di); 109 | $di->set('theme', $theme); 110 | 111 | 112 | $agent = new Engine\Agent($di); 113 | $di->set('agent', $agent); 114 | 115 | $file = new Engine\File($di); 116 | $di->set('file', $file); 117 | 118 | $profile = new Engine\Profile($di); 119 | $di->set('profile', $profile); 120 | 121 | 122 | // reponse 123 | $response = new Engine\Response($di); 124 | // register response header defined in app/service 125 | foreach (App\Config::header as $header) { 126 | $response->addHeader($header); 127 | } 128 | // setup compression level 0 - 9 129 | $response->setCompressionLevel(App\Config::compressLevel); 130 | $di->set('response', $response); 131 | 132 | 133 | // custom service provider register to di container 134 | foreach (glob(dirname(__DIR__) . '/App/Service/*.php') as $service) { 135 | $classname = basename($service, '.php'); 136 | $class = "App\\Service\\" . $classname; 137 | if (class_exists($class)) { 138 | $provider = new $class(); 139 | if ($provider instanceof \Engine\Interface\ServiceProviderInterface) { 140 | $provider->register($di); 141 | // $di->set($classname, new $class($di)); 142 | } 143 | } 144 | } 145 | 146 | // global controller hook and custom action before / after hook system 147 | // Todo: add remove hook and clear hook for a specific hook 148 | $hook = new Engine\Hook($di); 149 | // glob to load all the hook under the app/hook directory 150 | $hook->load(); 151 | $di->set('hook', $hook); 152 | 153 | // dispatch route from the query_string which from the current visiting url 154 | $router->dispatch($_SERVER['QUERY_STRING']); 155 | 156 | // output to client 157 | $response->output(); 158 | --------------------------------------------------------------------------------