├── extend └── .gitkeep ├── public ├── upload │ └── .gitkeep ├── static │ ├── css │ │ └── .gitkeep │ ├── font │ │ └── .gitkeep │ ├── js │ │ └── .gitkeep │ └── images │ │ └── .gitkeep ├── nginx.htaccess ├── .htaccess ├── index.php └── web.config ├── app ├── home │ ├── language │ │ └── zh_cn.php │ ├── model │ │ └── TestModel.php │ ├── controller │ │ └── IndexController.php │ └── view │ │ └── index │ │ └── index.php ├── admin │ ├── controller │ │ └── IndexController.php │ └── view │ │ └── index │ │ └── index.php ├── common │ └── BaseController.php └── middleware │ ├── AuthMiddleware.php │ └── LogMiddleware.php ├── vendor ├── composer │ ├── installed.json │ ├── autoload_namespaces.php │ ├── autoload_classmap.php │ ├── autoload_psr4.php │ ├── installed.php │ ├── platform_check.php │ ├── LICENSE │ ├── autoload_real.php │ ├── autoload_static.php │ ├── InstalledVersions.php │ └── ClassLoader.php └── autoload.php ├── config ├── view.php ├── cache.php ├── pagination.php ├── middleware.php ├── common.php ├── database.php └── route.php ├── function └── custom_function.php ├── startmvc ├── autoload.php ├── core │ ├── tpl │ │ ├── debug.php │ │ ├── error.php │ │ └── jump.php │ ├── Event.php │ ├── Response.php │ ├── Cache.php │ ├── Loader.php │ ├── Db.php │ ├── Logger.php │ ├── Pagination.php │ ├── db │ │ ├── DbCache.php │ │ └── DbInterface.php │ ├── Http.php │ ├── Container.php │ ├── Middleware.php │ ├── Upload.php │ ├── Controller.php │ ├── cache │ │ ├── File.php │ │ └── Redis.php │ ├── Cookie.php │ ├── Exception.php │ ├── Csrf.php │ ├── Request.php │ ├── Session.php │ ├── Config.php │ ├── App.php │ ├── Model.php │ ├── View.php │ └── Router.php ├── boot.php └── function.php ├── composer.json ├── README.md └── LICENSE /extend/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/upload/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/font/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/js/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/home/language/zh_cn.php: -------------------------------------------------------------------------------- 1 | 'startmvc轻量php框架', 6 | ]; -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [], 3 | "dev": true, 4 | "dev-package-names": [] 5 | } 6 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | '.php', // 模板文件后缀,默认php,可配置为html,.blade, .tpl, .twig, .phtml之类的 7 | ]; -------------------------------------------------------------------------------- /public/nginx.htaccess: -------------------------------------------------------------------------------- 1 | location /{ 2 | if (!-e $request_filename) { 3 | rewrite ^/(.*)$ /index.php/$1 last; 4 | break; 5 | } 6 | } -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine on 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^(.*)$ index.php [QSA,PT,L] 6 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /app/admin/controller/IndexController.php: -------------------------------------------------------------------------------- 1 | assign('admin',$admin); 10 | $this->display(); 11 | } 12 | } -------------------------------------------------------------------------------- /vendor/composer/autoload_psr4.php: -------------------------------------------------------------------------------- 1 | array($baseDir . '/startmvc'), 10 | 'extend\\' => array($baseDir . '/extend'), 11 | 'app\\' => array($baseDir . '/app'), 12 | ); 13 | -------------------------------------------------------------------------------- /app/admin/view/index/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {$admin} 6 | 7 | 8 | 9 | 10 | 11 | 12 | {$admin} 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/home/model/TestModel.php: -------------------------------------------------------------------------------- 1 | find('*',3); 11 | //return Model::find('*',3); 12 | //return $this->model('test')->find('*',3); 13 | //return self::find('*',2); 14 | return parent::find('*',3); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/home/controller/IndexController.php: -------------------------------------------------------------------------------- 1 | assign($data); 17 | $this->display(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /function/custom_function.php: -------------------------------------------------------------------------------- 1 | 'file', //默认驱动支持file,redis缓存 12 | 'file'=> [ 13 | 'cacheDir'=>'cache/', 14 | 'cacheTime'=>3600 15 | ], 16 | 'redis' => [ 17 | 'host' => '127.0.0.1', 18 | 'port' => 6379, 19 | 'password' => '', 20 | 'database' => 0, 21 | 'cacheTime'=>3600 22 | ], 23 | ]; -------------------------------------------------------------------------------- /config/pagination.php: -------------------------------------------------------------------------------- 1 | '%header% %first% %prev% %link% %next% %last%',//分页样式 14 | 'header' => '总共 %count% 页 %page% / %pageCount%',//分页头部 15 | 'first' => '首页',//首页 16 | 'last' => '末页',//末页 17 | 'prev' => '上一页',//上一页 18 | 'next' => '下一页',//下一页 19 | 'currentClass' => 'is-current',//当前页码类 20 | ]; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shaobingme/startmvc", 3 | "description": "A light-weight PHP MVC framework.", 4 | "type": "project", 5 | "keywords": [ 6 | "startmvc", 7 | "php框架", 8 | "framework", 9 | "mvc" 10 | ], 11 | "homepage": "http://www.startmvc.com/", 12 | "license": "Apache-2.0", 13 | "authors": [ 14 | { 15 | "name": "shaobing", 16 | "email": "startmvc@126.com" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.2.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "app\\": "app/", 25 | "extend\\": "extend/", 26 | "startmvc\\": "startmvc/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/common/BaseController.php: -------------------------------------------------------------------------------- 1 | _initialize(); 20 | } 21 | 22 | protected function _initialize() 23 | { 24 | // 子类可以重写此方法 25 | } 26 | } -------------------------------------------------------------------------------- /config/middleware.php: -------------------------------------------------------------------------------- 1 | [ 6 | // 'csrf' => 'app\\middleware\\CsrfMiddleware', 7 | // 'auth' => 'app\\middleware\\AuthMiddleware', 8 | // 'throttle' => 'app\\middleware\\ThrottleMiddleware', 9 | // ], 10 | 11 | // // 全局中间件(每个请求都会执行) 12 | // 'global' => [ 13 | // 'app\\middleware\\CsrfMiddleware', 14 | // ], 15 | 16 | // // 路由中间件(可以应用到特定路由) 17 | // 'route' => [ 18 | // 'auth' => 'app\\middleware\\AuthMiddleware', 19 | // 'throttle' => 'app\\middleware\\ThrottleMiddleware', 20 | // ] 21 | // ]; 22 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => 'shaobingme/startmvc', 4 | 'pretty_version' => 'dev-master', 5 | 'version' => 'dev-master', 6 | 'reference' => '430165126207e1a6c67a8124ad6ff055d16dd668', 7 | 'type' => 'project', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => true, 11 | ), 12 | 'versions' => array( 13 | 'shaobingme/startmvc' => array( 14 | 'pretty_version' => 'dev-master', 15 | 'version' => 'dev-master', 16 | 'reference' => '430165126207e1a6c67a8124ad6ff055d16dd668', 17 | 'type' => 'project', 18 | 'install_path' => __DIR__ . '/../../', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | ), 23 | ); 24 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {$title} 6 | 24 | 25 | 26 |
27 |

28 |

{$title}

29 | 36 |
37 | 38 | -------------------------------------------------------------------------------- /startmvc/core/tpl/debug.php: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

DEBUG

11 |

getMessage(); ?>

12 |
13 |
相关文件
14 |
getFile()))?>
15 | 16 |
错误位置
17 |
getLine() ?> 行
18 |
19 |
StartMVC框架
20 |
-------------------------------------------------------------------------------- /vendor/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 70200)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/middleware/AuthMiddleware.php: -------------------------------------------------------------------------------- 1 | authenticated = false; 27 | echo "用户未登录,但允许继续访问。
"; 28 | } else { 29 | $request->authenticated = true; 30 | echo "用户已登录。
"; 31 | } 32 | 33 | // 调用下一个中间件或控制器 34 | $response = $next($request); 35 | 36 | // 在响应返回前可以进行一些后处理 37 | // 例如:添加响应头、修改响应内容等 38 | 39 | return $response; 40 | } 41 | } -------------------------------------------------------------------------------- /vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /app/middleware/LogMiddleware.php: -------------------------------------------------------------------------------- 1 | $_SERVER['REQUEST_METHOD'], 27 | 'uri' => $_SERVER['REQUEST_URI'], 28 | 'ip' => $_SERVER['REMOTE_ADDR'], 29 | 'time' => date('Y-m-d H:i:s'), 30 | ]; 31 | 32 | // 输出请求信息(实际应用中应写入日志文件) 33 | echo ""; 34 | 35 | // 调用下一个中间件 36 | $response = $next($request); 37 | 38 | // 计算执行时间 39 | $endTime = microtime(true); 40 | $executionTime = round(($endTime - $startTime) * 1000, 2); 41 | 42 | // 输出执行时间信息 43 | echo ""; 44 | 45 | return $response; 46 | } 47 | } -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | return $loader; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/common.php: -------------------------------------------------------------------------------- 1 | true, //Debug模式,开发过程中开启,生产环境中请关闭 12 | 'trace' => true, //是否开启调试追踪,生产环境中请关闭 13 | 'timezone' => 'Asia/Shanghai', //系统时区 14 | 'url_suffix' => '.html', //URL后缀 15 | 'default_module' => 'home', //默认模块 16 | 'default_controller' => 'Index', //默认控制器 17 | 'default_action' => 'index', //默认方法 18 | 'urlrewrite' => true, //是否Url重写,隐藏index.php,需要服务器支持和对应的规则 19 | 'session_prefix' => '', //Session前缀 20 | 'cookie_prefix' => '', //Cookie前缀 21 | 'locale' => 'zh_cn', //指定默认语言,小写 22 | 'db_auto_connect' => true, //是否开启数据库自动连接 23 | 'theme' => '', //指定模板子目录,方便多风格使用,为空时模板文件在view下 24 | 25 | // Session安全配置 26 | 'session' => [ 27 | 'cookie_httponly' => true, // 防止JavaScript访问Cookie 28 | 'use_only_cookies' => true, // 只使用Cookie存储会话ID 29 | 'cookie_lifetime' => 7200, // 会话Cookie生存时间(秒) 30 | 'gc_maxlifetime' => 7200, // 垃圾回收时间(秒) 31 | ], 32 | // CSRF 防护配置 33 | 'csrf' => [ 34 | 'token_lifetime' => 3600, // Token有效期(秒) 35 | 'token_name' => 'csrf_token', // Token字段名 36 | 'auto_delete' => false, // 验证后是否自动删除(false=可重复使用,true=一次性) 37 | ], 38 | ]; -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'startmvc\\' => 9, 13 | ), 14 | 'e' => 15 | array ( 16 | 'extend\\' => 7, 17 | ), 18 | 'a' => 19 | array ( 20 | 'app\\' => 4, 21 | ), 22 | ); 23 | 24 | public static $prefixDirsPsr4 = array ( 25 | 'startmvc\\' => 26 | array ( 27 | 0 => __DIR__ . '/../..' . '/startmvc', 28 | ), 29 | 'extend\\' => 30 | array ( 31 | 0 => __DIR__ . '/../..' . '/extend', 32 | ), 33 | 'app\\' => 34 | array ( 35 | 0 => __DIR__ . '/../..' . '/app', 36 | ), 37 | ); 38 | 39 | public static $classMap = array ( 40 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 41 | ); 42 | 43 | public static function getInitializer(ClassLoader $loader) 44 | { 45 | return \Closure::bind(function () use ($loader) { 46 | $loader->prefixLengthsPsr4 = ComposerStaticInit40c38550ecb0b597f81db8b0fdbec3d6::$prefixLengthsPsr4; 47 | $loader->prefixDirsPsr4 = ComposerStaticInit40c38550ecb0b597f81db8b0fdbec3d6::$prefixDirsPsr4; 48 | $loader->classMap = ComposerStaticInit40c38550ecb0b597f81db8b0fdbec3d6::$classMap; 49 | 50 | }, null, ClassLoader::class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /startmvc/core/Event.php: -------------------------------------------------------------------------------- 1 | $callback) { 48 | $responses[] = call_user_func($callback, $payload); 49 | } 50 | } 51 | 52 | return $responses; 53 | } 54 | 55 | /** 56 | * 移除事件监听器 57 | * @param string $event 事件名称 58 | * @return void 59 | */ 60 | public static function forget($event) 61 | { 62 | unset(self::$listeners[$event]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /startmvc/core/tpl/error.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 系统错误 5 | 6 | 37 | 38 | 39 |
40 |

系统错误

41 |
42 | 43 |

错误信息:getMessage()); ?>

44 |

文件位置:getFile()); ?> 行号:getLine(); ?>

45 |
46 | 堆栈跟踪:
47 | getTraceAsString())); ?> 48 |
49 | 50 |

抱歉,系统遇到了一些问题。请稍后再试。

51 | 52 |
53 |
54 | 55 | -------------------------------------------------------------------------------- /startmvc/boot.php: -------------------------------------------------------------------------------- 1 | run(); -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | 'mysql',//指定数据库类型 5 | 'connections' => [ 6 | 'mysql' => [ 7 | 'driver' => 'mysql',//数据库类型 8 | 'host' => 'localhost',//数据库服务器地址 9 | 'database' => 'startmvc',//数据库名称 10 | 'username' => 'root',//数据库用户名 11 | 'password' => '123456',//数据库密码 12 | 'charset' => 'utf8',//数据库字符集 13 | 'port' => 3306, //数据库端口 14 | 'collation' => 'utf8_general_ci',//数据表编码 15 | 'prefix' => 'sm_',//数据表前缀 16 | 'cachetime' => 3600,//缓存时间(秒) 17 | 'cachedir' => ROOT_PATH . 'runtime'.DS.'db'.DS,//缓存目录(可选) 18 | 'options' => [ ]//连接选项(像SSL证书等可选) 19 | ], 20 | 'sqlite' => [ 21 | 'driver' => 'sqlite',//数据库类型 22 | 'database' => BASE_PATH.'data/database/test.db',//数据库文件路径 23 | 'prefix' => 'sm_',//数据表前缀 24 | 'cachetime' => 3600,//缓存时间(秒) 25 | 'cachedir' => ROOT_PATH . 'runtime'.DS.'db'.DS,//缓存目录(可选) 26 | 'options' => [ ]//连接选项(像SSL证书等可选) 27 | ], 28 | 'pgsql' => [ 29 | 'driver' => 'pgsql',//数据库类型 30 | 'host' => 'localhost',//数据库服务器地址 31 | 'database' => 'startmvc',//数据库名称 32 | 'username' => 'root',//数据库用户名 33 | 'password' => '',//数据库密码 34 | 'charset' => 'utf8',//数据库字符集 35 | 'port' => 3306, //数据库端口 36 | 'collation' => 'utf8_general_ci',//数据表编码 37 | 'prefix' => 'sm_',//数据表前缀 38 | 'cachetime' => 3600,//缓存时间(秒) 39 | 'cachedir' => ROOT_PATH . 'runtime'.DS.'db'.DS,//缓存目录(可选) 40 | 'options' => [ ]//连接选项(像SSL证书等可选) 41 | ], 42 | 'oracle' => [ 43 | 'driver' => 'oracle',//数据库类型 44 | 'host' => 'localhost:8000',//数据库服务器地址 45 | 'database' => 'startmvc',//数据库名称 46 | 'username' => 'root',//数据库用户名 47 | 'password' => '',//数据库密码 48 | 'charset' => 'utf8',//数据库字符集 49 | 'port' => 3306, //数据库端口 50 | 'collation' => 'utf8_general_ci',//数据表编码 51 | 'prefix' => 'sm_',//数据表前缀 52 | 'cachetime' => 3600,//缓存时间(秒) 53 | 'cachedir' => ROOT_PATH . 'runtime'.DS.'db'.DS,//缓存目录(可选) 54 | 'options' => [ ]//连接选项(像SSL证书等可选) 55 | ], 56 | 57 | ], 58 | ]; -------------------------------------------------------------------------------- /startmvc/core/tpl/jump.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 页面跳转... 9 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | :( 49 | 50 | :) 51 | 52 | 53 | 54 |
55 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /startmvc/core/Response.php: -------------------------------------------------------------------------------- 1 | statusCode = $code; 40 | return $this; 41 | } 42 | 43 | /** 44 | * 设置响应头 45 | * @param string $key 头名 46 | * @param string $value 头值 47 | * @return $this 48 | */ 49 | public function setHeader($key, $value) 50 | { 51 | $this->headers[$key] = $value; 52 | return $this; 53 | } 54 | 55 | /** 56 | * 设置内容 57 | * @param string $content 响应内容 58 | * @return $this 59 | */ 60 | public function setContent($content) 61 | { 62 | $this->content = $content; 63 | return $this; 64 | } 65 | 66 | /** 67 | * 发送响应 68 | * @return void 69 | */ 70 | public function send() 71 | { 72 | // 设置状态码 73 | http_response_code($this->statusCode); 74 | 75 | // 设置响应头 76 | foreach ($this->headers as $key => $value) { 77 | header("$key: $value"); 78 | } 79 | 80 | // 输出内容 81 | echo $this->content; 82 | } 83 | 84 | /** 85 | * 返回JSON响应 86 | * @param mixed $data 数据 87 | * @param int $status 状态码 88 | * @return $this 89 | */ 90 | public function json($data, $status = 200) 91 | { 92 | $this->setHeader('Content-Type', 'application/json'); 93 | $this->setStatusCode($status); 94 | $this->setContent(json_encode($data)); 95 | return $this; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /startmvc/core/Cache.php: -------------------------------------------------------------------------------- 1 | drive = new $className($params); 39 | } 40 | 41 | /** 42 | * 设置缓存 43 | * @param string $key 缓存键名 44 | * @param mixed $val 缓存数据 45 | * @return $this 46 | */ 47 | public function set(string $key, $val) { 48 | $this->drive->set($key, $val); 49 | return $this; 50 | } 51 | 52 | /** 53 | * 检查缓存是否存在 54 | * @param string $key 缓存键名 55 | * @return bool 56 | */ 57 | public function has(string $key) { 58 | return $this->drive->has($key); 59 | } 60 | 61 | /** 62 | * 获取缓存 63 | * @param string $key 缓存键名 64 | * @return mixed 65 | */ 66 | public function get(string $key) { 67 | return $this->drive->get($key); 68 | } 69 | 70 | /** 71 | * 删除缓存 72 | * @param string $key 缓存键名 73 | * @return $this 74 | */ 75 | public function delete(string $key) { 76 | $this->drive->delete($key); 77 | return $this; 78 | } 79 | 80 | /** 81 | * 清空所有缓存 82 | * @return $this 83 | */ 84 | public function clear() { 85 | $this->drive->clear(); 86 | return $this; 87 | } 88 | 89 | /** 90 | * 创建缓存实例的静态方法 91 | * @param string $driver 驱动名称 92 | * @param array $params 驱动参数 93 | * @return Cache 94 | */ 95 | public static function store(string $driver = null, array $params = []) 96 | { 97 | return new self($driver, $params); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /startmvc/core/Loader.php: -------------------------------------------------------------------------------- 1 | newInstanceArgs($paramArr); 19 | } 20 | 21 | public static function make($controller, $action, $argv) 22 | { 23 | try { 24 | $class = new \ReflectionClass($controller); 25 | $instance = $class->newInstanceArgs(); 26 | 27 | if (!method_exists($instance, $action)) { 28 | throw new \Exception("方法{$action}不存在"); 29 | } 30 | 31 | return call_user_func_array([$instance, $action], $argv); 32 | } catch (\ReflectionException $e) { 33 | throw new \Exception("控制器实例化失败:" . $e->getMessage()); 34 | } 35 | } 36 | 37 | protected static function getMethodParams($className, $methodsName = '__construct') 38 | { 39 | $class = new \ReflectionClass($className); 40 | $paramArr = []; 41 | if ($class->hasMethod($methodsName)) { 42 | $method = $class->getMethod($methodsName); 43 | $params = $method->getParameters(); 44 | if (count($params) > 0) { 45 | foreach ($params as $key => $param) { 46 | // 使用 getType() 代替 getClass() 47 | $type = $param->getType(); 48 | if ($type && !$type->isBuiltin() && $type instanceof \ReflectionNamedType) { 49 | $paramClassName = $type->getName(); 50 | $args = self::getMethodParams($paramClassName); 51 | $paramArr[] = (new \ReflectionClass($paramClassName))->newInstanceArgs($args); 52 | } 53 | } 54 | } 55 | } 56 | return $paramArr; 57 | } 58 | 59 | protected static function filter($doc) 60 | { 61 | if ($doc) { 62 | preg_match_all('/filter\[[\S\s]+\]/U', $doc, $matches); 63 | foreach ($matches[0] as $filter) { 64 | $filterClass = preg_replace('/filter\[([\S\s]+)\(([\S\s]*)\)\]/', '${1}', $filter); 65 | $filterClass = '\\Filter\\' . $filterClass; 66 | $filterParamArr = preg_replace('/filter\[([\S\s]+)\(([\S\s]*)\)\]/', '${2}', $filter); 67 | $filterParamArr = explode(',', $filterParamArr); 68 | for ($i = 0; $i < count($filterParamArr); $i++) { 69 | $filterParamArr[$i] = trim($filterParamArr[$i]); 70 | } 71 | $instance = self::getInstance($filterClass); 72 | if (method_exists($instance, 'handle')) { 73 | $instance->handle(...$filterParamArr); 74 | } 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /startmvc/core/Db.php: -------------------------------------------------------------------------------- 1 | table($table); 31 | } 32 | 33 | return $instance; 34 | } 35 | 36 | /** 37 | * 获取数据库表实例 38 | * @param string $table 表名 39 | * @param array $config 数据库配置 40 | * @return DbCore 41 | */ 42 | public static function table($table, $config = []) 43 | { 44 | return static::getInstance($config)->table($table); 45 | } 46 | 47 | /** 48 | * 获取数据库实例 49 | * @param array $config 数据库配置 50 | * @return DbCore 51 | */ 52 | protected static function getInstance($config = []) 53 | { 54 | // 生成配置的唯一标识 55 | $configKey = empty($config) ? 'default' : md5(serialize($config)); 56 | 57 | if (!isset(static::$instances[$configKey])) { 58 | if (empty($config)) { 59 | // 使用默认配置 60 | $defaultConfig = include CONFIG_PATH . '/database.php'; 61 | if (isset($defaultConfig['driver']) && $defaultConfig['driver'] !== '') { 62 | $config = $defaultConfig['connections'][$defaultConfig['driver']]; 63 | } else { 64 | throw new \Exception('数据库配置不正确,请检查配置文件'); 65 | } 66 | } 67 | static::$instances[$configKey] = DbCore::getInstance($config); 68 | } 69 | 70 | return static::$instances[$configKey]; 71 | } 72 | 73 | /** 74 | * 检查数据表是否存在 75 | * @param string $table 表名 76 | * @param array $config 数据库配置(可选) 77 | * @return bool 78 | */ 79 | public static function is_table($table, $config = []) 80 | { 81 | return static::getInstance($config)->is_table($table); 82 | } 83 | 84 | /** 85 | * 调用DbCore的其他方法 86 | * @param string $method 方法名 87 | * @param array $args 参数 88 | * @return mixed 89 | */ 90 | public static function __callStatic($method, $args) 91 | { 92 | return call_user_func_array([static::getInstance(), $method], $args); 93 | } 94 | } -------------------------------------------------------------------------------- /startmvc/core/Logger.php: -------------------------------------------------------------------------------- 1 | path = $path ?: ROOT_PATH . 'runtime/logs'; 25 | 26 | // 确保目录存在 27 | if (!is_dir($this->path)) { 28 | mkdir($this->path, 0777, true); 29 | } 30 | } 31 | 32 | /** 33 | * 写入日志 34 | * @param string $level 日志级别 35 | * @param string $message 日志消息 36 | * @param array $context 上下文数据 37 | * @return bool 38 | */ 39 | public function log($level, $message, array $context = []) 40 | { 41 | if (!in_array($level, $this->levels)) { 42 | throw new \InvalidArgumentException("无效的日志级别 [$level]"); 43 | } 44 | 45 | // 格式化消息 46 | $message = $this->formatMessage($level, $message, $context); 47 | 48 | // 写入文件 49 | $file = $this->path . '/' . date('Y-m-d') . '.log'; 50 | return file_put_contents($file, $message . PHP_EOL, FILE_APPEND | LOCK_EX); 51 | } 52 | 53 | /** 54 | * 格式化日志消息 55 | * @param string $level 日志级别 56 | * @param string $message 日志消息 57 | * @param array $context 上下文数据 58 | * @return string 59 | */ 60 | protected function formatMessage($level, $message, array $context = []) 61 | { 62 | // 替换上下文变量 63 | $replace = []; 64 | foreach ($context as $key => $val) { 65 | if (is_string($val) || is_numeric($val)) { 66 | $replace['{' . $key . '}'] = $val; 67 | } 68 | } 69 | 70 | $message = strtr($message, $replace); 71 | 72 | return '[' . date('Y-m-d H:i:s') . '] ' . strtoupper($level) . ': ' . $message; 73 | } 74 | 75 | /** 76 | * 记录调试信息 77 | * @param string $message 日志消息 78 | * @param array $context 上下文数据 79 | * @return bool 80 | */ 81 | public function debug($message, array $context = []) 82 | { 83 | return $this->log('debug', $message, $context); 84 | } 85 | 86 | /** 87 | * 记录错误信息 88 | * @param string $message 日志消息 89 | * @param array $context 上下文数据 90 | * @return bool 91 | */ 92 | public function error($message, array $context = []) 93 | { 94 | return $this->log('error', $message, $context); 95 | } 96 | 97 | // 其他级别的快捷方法... 98 | } -------------------------------------------------------------------------------- /startmvc/core/Pagination.php: -------------------------------------------------------------------------------- 1 | theme =isset($config['theme'])?$config['theme']:'%header% %first% %prev% %link% %next% %last%'; 17 | $this->header =isset($config['header'])?$config['header']:'共 %count% 条记录 第 %page% / %pageCount% 页'; 18 | $this->first =isset($config['first'])?$config['first']:'首页'; 19 | $this->last =isset($config['last'])?$config['last']:'末页'; 20 | $this->prev =isset($config['prev'])?$config['prev']:'上一页'; 21 | $this->next =isset($config['next'])?$config['next']:'下一页'; 22 | $this->currentClass =isset($config['currentClass'])?$config['currentClass']:'current'; 23 | } 24 | function Show($count, $pageSize, $page, $url, $pageShowCount = 10){ 25 | $pageCount = ceil($count / $pageSize); 26 | $header = '' . str_replace(['%count%', '%page%', '%pageCount%'], [$count, $page, $pageCount], $this->header) . ''; 27 | $first = '' . $this->first . ''; 28 | $last = '' . $this->last . ''; 29 | $prev = ' 1 ? ' href="' . $this->url($url, $page - 1) . '"' : '') . '>' . $this->prev . ''; 30 | $next = '' . $this->next . ''; 31 | 32 | $link = ''; 33 | $start = $page - $pageShowCount / 2; 34 | $start = $start < 1 ? 1 : $start; 35 | $end = $start + $pageShowCount - 1; 36 | if($end > $pageCount){ 37 | $end = $pageCount; 38 | $start = $end - $pageShowCount + 1; 39 | $start = $start < 1 ? 1 : $start; 40 | } 41 | for($p = $start; $p <= $end; $p++){ 42 | $link .= 'currentClass . '"'; 45 | else 46 | $link .= ' href="' . $this->url($url, $p) . '"'; 47 | $link .= '>' . $p . ''; 48 | } 49 | return str_replace([ 50 | '%header%', '%first%', '%prev%', '%link%', '%next%', '%last%' 51 | ], [ 52 | $header, $first, $prev, $link, $next, $last 53 | ], $this->theme); 54 | } 55 | function url($url, $page){ 56 | return str_replace('{page}', $page, urldecode($url)); 57 | } 58 | } -------------------------------------------------------------------------------- /startmvc/core/db/DbCache.php: -------------------------------------------------------------------------------- 1 | cacheDir = $dir; 50 | $this->cache = $time; 51 | $this->finish = time() + $time; 52 | } 53 | 54 | /** 55 | * 获取缓存数据 56 | * 57 | * @param $sql SQL查询语句 58 | * @param bool $array 是否返回数组 59 | * 60 | * @return bool|void 61 | */ 62 | public function getCache($sql, $array = false) 63 | { 64 | if (is_null($this->cache)) { 65 | return false; 66 | } 67 | 68 | $cacheFile = $this->cacheDir . $this->fileName($sql) . '.cache'; 69 | if (file_exists($cacheFile)) { 70 | $cache = json_decode(file_get_contents($cacheFile), $array); 71 | 72 | if (($array ? $cache['finish'] : $cache->finish) < time()) { 73 | unlink($cacheFile); 74 | return; 75 | } 76 | 77 | return ($array ? $cache['data'] : $cache->data); 78 | } 79 | 80 | return false; 81 | } 82 | 83 | /** 84 | * 设置缓存数据 85 | * 86 | * @param $sql SQL查询语句 87 | * @param $result 查询结果 88 | * 89 | * @return bool|void 90 | */ 91 | public function setCache($sql, $result) 92 | { 93 | if (is_null($this->cache)) { 94 | return false; 95 | } 96 | 97 | $cacheFile = $this->cacheDir . $this->fileName($sql) . '.cache'; 98 | $cacheFile = fopen($cacheFile, 'w'); 99 | 100 | if ($cacheFile) { 101 | fputs($cacheFile, json_encode(['data' => $result, 'finish' => $this->finish])); 102 | } 103 | 104 | return; 105 | } 106 | 107 | /** 108 | * 根据SQL生成缓存文件名 109 | * 110 | * @param $name SQL查询语句 111 | * 112 | * @return string 113 | */ 114 | protected function fileName($name) 115 | { 116 | return md5($name); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /startmvc/core/db/DbInterface.php: -------------------------------------------------------------------------------- 1 | bindings[$abstract] = compact('concrete', 'shared'); 26 | } 27 | 28 | /** 29 | * 注册一个共享绑定(单例) 30 | * @param string $abstract 抽象类型 31 | * @param mixed $concrete 具体实现 32 | * @return void 33 | */ 34 | public function singleton($abstract, $concrete = null) 35 | { 36 | $this->bind($abstract, $concrete, true); 37 | } 38 | 39 | /** 40 | * 解析一个类型的实例 41 | * @param string $abstract 要解析的类型 42 | * @param array $parameters 构造函数参数 43 | * @return mixed 解析出的实例 44 | * @throws BindingResolutionException 45 | */ 46 | public function make($abstract, array $parameters = []) 47 | { 48 | if (isset(static::$instances[$abstract])) { 49 | return static::$instances[$abstract]; 50 | } 51 | 52 | $concrete = $this->getConcrete($abstract); 53 | $object = $this->build($concrete, $parameters); 54 | 55 | if ($this->isShared($abstract)) { 56 | static::$instances[$abstract] = $object; 57 | } 58 | 59 | return $object; 60 | } 61 | 62 | /** 63 | * 增加自动解析构造函数参数的能力 64 | * @param string $concrete 具体类名 65 | * @param array $parameters 手动提供的参数 66 | * @return object 实例化对象 67 | */ 68 | protected function build($concrete, array $parameters = []) 69 | { 70 | // 如果是闭包,直接执行 71 | if ($concrete instanceof \Closure) { 72 | return $concrete($this, $parameters); 73 | } 74 | 75 | // 获取反射类 76 | $reflector = new \ReflectionClass($concrete); 77 | 78 | // 检查是否可实例化 79 | if (!$reflector->isInstantiable()) { 80 | throw new \Exception("类 {$concrete} 不可实例化"); 81 | } 82 | 83 | // 获取构造函数 84 | $constructor = $reflector->getConstructor(); 85 | 86 | // 如果没有构造函数,直接实例化 87 | if (is_null($constructor)) { 88 | return new $concrete; 89 | } 90 | 91 | // 获取构造函数参数 92 | $dependencies = $constructor->getParameters(); 93 | 94 | // 解析构造函数的依赖 95 | $instances = $this->resolveDependencies($dependencies, $parameters); 96 | 97 | // 创建实例 98 | return $reflector->newInstanceArgs($instances); 99 | } 100 | } -------------------------------------------------------------------------------- /startmvc/core/Middleware.php: -------------------------------------------------------------------------------- 1 | handle($request, $next); 91 | }; 92 | 93 | return $firstSlice($request); 94 | } 95 | 96 | /** 97 | * 执行所有全局中间件 98 | * @param object $request 请求对象 99 | * @param \Closure $destination 最终目标处理函数 100 | * @return mixed 101 | */ 102 | public static function run($request, \Closure $destination) 103 | { 104 | return self::pipeline(self::$middleware, $request, $destination); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /startmvc/core/Upload.php: -------------------------------------------------------------------------------- 1 | $value) { 25 | if (property_exists($this, $key)) { 26 | $this->$key = $value; 27 | } 28 | } 29 | } 30 | 31 | function upload() { 32 | $results = []; 33 | foreach ($_FILES as $file) { 34 | if (is_array($file['name'])) { 35 | foreach ($file['name'] as $key => $value) { 36 | $fileInfo = []; 37 | foreach ($file as $k => $v) { 38 | $fileInfo[$k] = $v[$key]; 39 | } 40 | $results[] = $this->file($fileInfo); 41 | } 42 | } else { 43 | $results[] = $this->file($file); 44 | } 45 | } 46 | return $results; 47 | } 48 | 49 | private function file($file) { 50 | if ($file['error'] !== UPLOAD_ERR_OK) { 51 | return ['result' => false, 'error' => '文件上传错误: ' . $file['error']]; 52 | } 53 | 54 | $fileExt = pathinfo($file['name'], PATHINFO_EXTENSION); 55 | if (!in_array($fileExt, $this->exts)) { 56 | return ['result' => false, 'error' => '无效的文件扩展名']; 57 | } 58 | 59 | $saveDir = rtrim($this->savePath, '/') . '/'; 60 | $saveUrl = rtrim($this->urlPath, '/') . '/'; 61 | 62 | if ($this->autoSub) { 63 | $subDir = date('Y/m/d'); 64 | $saveDir .= $subDir; 65 | $saveUrl .= $subDir; 66 | } 67 | if (!is_dir($saveDir)&&!mkdir($saveDir, 0755, true)) { 68 | return ['result' => false, 'error' => '创建目录失败']; 69 | } 70 | 71 | //$filename = $this->autoName ? uniqid() . '.' . $fileExt : $file['name']; 72 | $filename = $this->fileName !== '' ? $this->fileName.'.'. $fileExt : ($this->autoName ? uniqid() . '.' . $fileExt : $file['name']); 73 | $filePath = $saveDir . '/' . $filename; 74 | $urlPath = $saveUrl . '/' . $filename; 75 | 76 | if (!$this->replace && file_exists($filePath)) { 77 | return ['result' => false, 'error' => '文件已经存在']; 78 | } 79 | 80 | if (!move_uploaded_file($file['tmp_name'], $filePath)) { 81 | return ['result' => false, 'error' => '移动上传文件失败']; 82 | } 83 | 84 | return ['result' => true, 'url' => $urlPath,'filename'=>$filename]; 85 | } 86 | } -------------------------------------------------------------------------------- /startmvc/core/Controller.php: -------------------------------------------------------------------------------- 1 | conf = config(); 25 | $this->view = new View(); 26 | } 27 | /** 28 | * 模型定义 29 | */ 30 | protected function model($model, $module = MODULE) 31 | { 32 | $model = APP_NAMESPACE.'\\' . $module . '\\'. 'model\\' . $model . 'Model'; 33 | return Loader::getInstance($model); 34 | } 35 | /** 36 | * url的方法 37 | */ 38 | protected function url($url) 39 | { 40 | $url = $url . config('url_suffix'); 41 | if (config('urlrewrite')) { 42 | $url = '/' . $url; 43 | } else { 44 | $url = '/index.php/' . $url; 45 | } 46 | return str_replace('%2F', '/', urlencode($url)); 47 | } 48 | 49 | /** 50 | * 为模板对象赋值 51 | */ 52 | protected function assign($name=[], $data='') 53 | { 54 | $this->view->assign($name, $data); 55 | return $this; // 支持链式调用 56 | } 57 | 58 | /** 59 | * 调用视图 60 | */ 61 | 62 | protected function display($tplfile='',$data=[]) 63 | { 64 | // 直接调用视图的display方法,输出内容 65 | $this->view->display($tplfile,$data); 66 | 67 | // 如果开启了 trace,在页面末尾添加 trace 信息 68 | if (config('trace')) { 69 | \startmvc\core\App::outputTrace(); 70 | } 71 | } 72 | 73 | /** 74 | * 获取渲染内容但不输出 75 | */ 76 | protected function fetch($tplfile='',$data=[]) 77 | { 78 | return $this->view->fetch($tplfile,$data); 79 | } 80 | 81 | /** 82 | * 调用内容 83 | */ 84 | public function content($content) 85 | { 86 | header('Content-Type:text/plain; charset=utf-8'); 87 | echo $content; 88 | } 89 | protected function success($msg='',$url='',$data=[],$ajax=false) 90 | { 91 | $this->response(1,$msg,$url,$data,$ajax); 92 | } 93 | protected function error($msg='',$url='',$data=[],$ajax=false) 94 | { 95 | $this->response(0,$msg,$url,$data,$ajax); 96 | } 97 | protected function response($code='',$msg='',$url='',$data=[],$ajax=false) 98 | { 99 | if($ajax || Request::isAjax()){ 100 | $data=[ 101 | 'code'=>$code,//1-成功 0-失败 102 | 'msg'=>$msg, 103 | 'url'=>$url, 104 | 'data'=>$data, 105 | ]; 106 | $this->json($data); 107 | }else{ 108 | include __DIR__.DS.'tpl/jump.php'; 109 | exit(); 110 | } 111 | 112 | } 113 | 114 | /** 115 | * json方法 116 | */ 117 | protected function json($data) 118 | { 119 | header('Content-Type:application/json; charset=utf-8'); 120 | //echo json_encode($data, JSON_UNESCAPED_UNICODE); 121 | exit(json_encode($data, JSON_UNESCAPED_UNICODE)); 122 | } 123 | 124 | 125 | /** 126 | * 跳转 127 | */ 128 | protected function redirect($url='') 129 | { 130 | $url=$url?:'/'; 131 | header('location:' . $url); 132 | exit(); 133 | } 134 | /** 135 | * 404方法 136 | */ 137 | protected function notFound() 138 | { 139 | header("HTTP/1.1 404 Not Found"); 140 | header("Status: 404 Not Found"); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /startmvc/core/cache/File.php: -------------------------------------------------------------------------------- 1 | cacheDir = ROOT_PATH . '/runtime/' . $params['cacheDir']; 32 | $this->cacheTime = $params['cacheTime']; 33 | 34 | if (!file_exists($this->cacheDir)) { 35 | mkdir($this->cacheDir, 0777, true); 36 | } 37 | } 38 | 39 | /** 40 | * 获取缓存文件路径 41 | * @param string $key 缓存键名 42 | * @return string 缓存文件路径 43 | */ 44 | private function getPath($key) { 45 | return $this->cacheDir . md5($key) . '.cache'; 46 | } 47 | 48 | /** 49 | * 设置缓存 50 | * @param string $key 缓存键名 51 | * @param mixed $data 缓存数据 52 | * @return void 53 | */ 54 | public function set($key, $data) { 55 | $cacheFile = $this->getPath($key); 56 | $cacheData = [ 57 | 'data' => $data, 58 | 'expire' => time() + $this->cacheTime 59 | ]; 60 | file_put_contents($cacheFile, serialize($cacheData)); 61 | } 62 | 63 | /** 64 | * 获取缓存 65 | * @param string $key 缓存键名 66 | * @return mixed 缓存数据,不存在或已过期返回null 67 | */ 68 | public function get($key) { 69 | $cacheFile = $this->getPath($key); 70 | 71 | if (!file_exists($cacheFile)) { 72 | return null; 73 | } 74 | 75 | $cacheData = unserialize(file_get_contents($cacheFile)); 76 | 77 | // 检查是否过期 78 | if (time() > $cacheData['expire']) { 79 | $this->delete($key); 80 | return null; 81 | } 82 | 83 | return $cacheData['data']; 84 | } 85 | 86 | /** 87 | * 检查缓存是否存在且有效 88 | * @param string $key 缓存键名 89 | * @return bool 90 | */ 91 | public function has(string $key): bool { 92 | $cacheFile = $this->getPath($key); 93 | 94 | if (!file_exists($cacheFile)) { 95 | return false; 96 | } 97 | 98 | $cacheData = unserialize(file_get_contents($cacheFile)); 99 | return time() <= $cacheData['expire']; 100 | } 101 | 102 | /** 103 | * 删除缓存 104 | * @param string $key 缓存键名 105 | * @return void 106 | */ 107 | public function delete($key) { 108 | $cacheFile = $this->getPath($key); 109 | if (file_exists($cacheFile)) { 110 | unlink($cacheFile); 111 | } 112 | } 113 | 114 | /** 115 | * 清空所有缓存 116 | * @return void 117 | */ 118 | public function clear() { 119 | $files = glob($this->cacheDir . '*.cache'); 120 | foreach ($files as $file) { 121 | unlink($file); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /startmvc/core/cache/Redis.php: -------------------------------------------------------------------------------- 1 | redis = new \Redis(); 38 | 39 | // 连接Redis服务器 40 | $connect = $this->redis->connect($params['host'], $params['port']); 41 | if (!$connect) { 42 | throw new \Exception('Redis连接失败'); 43 | } 44 | 45 | // 设置认证和数据库 46 | if (!empty($params['password'])) { 47 | $this->redis->auth($params['password']); 48 | } 49 | 50 | $this->redis->select((int)$params['database']); 51 | $this->cacheTime = $params['cacheTime']; 52 | } 53 | 54 | /** 55 | * 获取带前缀的缓存键名 56 | * @param string $key 原始键名 57 | * @return string 带前缀的键名 58 | */ 59 | private function getKey($key) { 60 | return 'cache:' . md5($key); 61 | } 62 | 63 | /** 64 | * 设置缓存 65 | * @param string $key 缓存键名 66 | * @param mixed $data 缓存数据 67 | * @return bool 是否成功 68 | */ 69 | public function set($key, $data) { 70 | $cacheKey = $this->getKey($key); 71 | $cacheData = [ 72 | 'data' => $data, 73 | 'expire' => time() + $this->cacheTime 74 | ]; 75 | 76 | return $this->redis->set($cacheKey, serialize($cacheData), $this->cacheTime); 77 | } 78 | 79 | /** 80 | * 获取缓存 81 | * @param string $key 缓存键名 82 | * @return mixed 缓存数据,不存在或已过期返回null 83 | */ 84 | public function get($key) { 85 | $cacheKey = $this->getKey($key); 86 | $cacheData = $this->redis->get($cacheKey); 87 | 88 | if ($cacheData === false) { 89 | return null; 90 | } 91 | 92 | $cacheData = unserialize($cacheData); 93 | 94 | // 检查是否过期(双重检查,Redis自身会过期,这里是额外保障) 95 | if (time() > $cacheData['expire']) { 96 | $this->redis->del($cacheKey); 97 | return null; 98 | } 99 | 100 | return $cacheData['data']; 101 | } 102 | 103 | /** 104 | * 检查缓存是否存在且有效 105 | * @param string $key 缓存键名 106 | * @return bool 107 | */ 108 | public function has(string $key): bool { 109 | return $this->get($key) !== null; 110 | } 111 | 112 | /** 113 | * 删除缓存 114 | * @param string $key 缓存键名 115 | * @return bool 是否成功 116 | */ 117 | public function delete($key) { 118 | $cacheKey = $this->getKey($key); 119 | return $this->redis->del($cacheKey) > 0; 120 | } 121 | 122 | /** 123 | * 清空所有缓存 124 | * @param bool $onlyCache 是否只清除缓存前缀的键 125 | * @return bool 是否成功 126 | */ 127 | public function clear($onlyCache = true) { 128 | if ($onlyCache) { 129 | // 只清除缓存前缀的键 130 | $keys = $this->redis->keys('cache:*'); 131 | if (!empty($keys)) { 132 | return $this->redis->del($keys) > 0; 133 | } 134 | return true; 135 | } 136 | 137 | // 清除整个数据库(谨慎使用) 138 | return $this->redis->flushDB(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /startmvc/core/Cookie.php: -------------------------------------------------------------------------------- 1 | $value) { 108 | if (strpos($key, $prefix) === 0) { 109 | $newKey = substr($key, $prefixLength); 110 | $result[$newKey] = $value; 111 | } 112 | } 113 | 114 | return $result; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /startmvc/core/Exception.php: -------------------------------------------------------------------------------- 1 | getMessage(), 70 | $exception->getFile(), 71 | $exception->getLine(), 72 | $exception->getTraceAsString() 73 | ); 74 | 75 | $logFile = $logPath . DIRECTORY_SEPARATOR . date('Y-m-d') . '_error.log'; 76 | 77 | // 使用错误抑制符,避免因写入失败导致的额外异常 78 | @error_log($message, 3, $logFile); 79 | } 80 | 81 | /** 82 | * 处理异常 83 | * @param \Throwable $exception 84 | */ 85 | public static function handleException(\Throwable $exception) 86 | { 87 | try { 88 | self::logException($exception); 89 | } catch (\Exception $e) { 90 | // 日志记录失败时的处理 91 | } 92 | 93 | // 设置HTTP状态码 94 | http_response_code(500); 95 | 96 | // 获取调试模式设置 97 | $debug = config('debug', true); // 默认为true,确保在配置不存在时也能看到错误 98 | 99 | // AJAX请求处理 100 | if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 101 | strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { 102 | header('Content-Type: application/json'); 103 | echo json_encode([ 104 | 'error' => $exception->getMessage(), 105 | 'trace' => $exception->getTraceAsString() 106 | ]); 107 | exit; 108 | } 109 | 110 | // 传递异常对象到错误模板 111 | $e = $exception; // 为错误模板提供异常对象 112 | 113 | // 包含错误模板 114 | $errorTemplate = CORE_PATH . 'tpl/error.php'; 115 | if (file_exists($errorTemplate)) { 116 | include $errorTemplate; 117 | } else { 118 | echo '

系统错误

'; 119 | echo '

' . htmlspecialchars($exception->getMessage()) . '

'; 120 | echo '
' . htmlspecialchars($exception->getTraceAsString()) . '
'; 121 | } 122 | exit; 123 | } 124 | 125 | /** 126 | * 处理程序结束时的错误 127 | */ 128 | public static function handleShutdown() 129 | { 130 | $error = error_get_last(); 131 | 132 | if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) { 133 | self::handleError($error['type'], $error['message'], $error['file'], $error['line']); 134 | } 135 | } 136 | 137 | /** 138 | * 显示友好的错误页面给用户(在生产环境中使用) 139 | */ 140 | private static function errorPage($output) 141 | { 142 | // 将错误信息作为 GET 参数传递到错误页面 143 | //$errorPageURL = '/error-page.php?error=' . urlencode($errorMessage); 144 | //header("Location: $errorPageURL"); 145 | if(config('debug')){ 146 | include 'tpl/error.php'; 147 | } 148 | exit; 149 | } 150 | } 151 | 152 | // 创建 CustomErrorHandler 实例,自动注册错误处理和异常处理方法 153 | //$customErrorHandler = new CustomErrorHandler(); -------------------------------------------------------------------------------- /config/route.php: -------------------------------------------------------------------------------- 1 | home/article/index/123 29 | ['category/(:num)', 'home/category/index/$1'], // category/123 -> home/category/index/123 30 | ['news/(:num)', 'home/news/detail/$1'], // news/123 -> home/news/detail/123 31 | ['user/(:num)', 'home/user/profile/$1'], // user/123 -> home/user/profile/123 32 | 33 | // 2. 多级路径路由 34 | ['article/detail/(:num)', 'home/article/detail/$1'], // article/detail/123 -> home/article/detail/123 35 | ['product/(:alpha)/(:num)', 'home/product/show/$1/$2'], // product/phone/123 -> home/product/show/phone/123 36 | ['blog/(:any)/(:num)', 'home/blog/detail/$1/$2'], // blog/tech/123 -> home/blog/detail/tech/123 37 | 38 | // 3. 字母参数路由 39 | ['tag/(:alpha)', 'home/tag/index/$1'], // tag/tech -> home/tag/index/tech 40 | ['lang/(:alpha)', 'home/index/lang/$1'], // lang/en -> home/index/lang/en 41 | 42 | // 4. 任意字符路由 43 | ['search/(:any)', 'home/search/index/$1'], // search/keyword -> home/search/index/keyword 44 | ['page/(:any)', 'home/page/show/$1'], // page/about -> home/page/show/about 45 | 46 | // 5. 隐藏默认模块路由(将所有请求映射到home模块) 47 | // ['(:any)', 'home/$1'], // 任意路径 -> home/任意路径 48 | 49 | 50 | // ==================== 正则表达式路由规则 ==================== 51 | 52 | // 1. 精确匹配 53 | ['/^about$/', 'home/index/about'], // 精确匹配 about 54 | ['/^contact$/', 'home/index/contact'], // 精确匹配 contact 55 | 56 | // 2. 数字参数匹配 57 | ['/^article_(\d+)$/', 'home/article/index/$1'], // article_123 58 | ['/^column\/(\d+)$/', 'home/column/index/$1'], // column/123 59 | ['/^category\/(\d+)$/', 'home/category/index/$1'], // category/123 60 | 61 | // 3. 多参数匹配 62 | ['/^product\/([a-zA-Z]+)\/(\d+)$/', 'home/product/detail/$1/$2'], // product/phone/123 63 | ['/^blog\/([^\/]+)\/(\d+)$/', 'home/blog/detail/$1/$2'], // blog/tech/123 64 | 65 | // 4. 可选参数匹配 66 | ['/^list\/(\d+)?$/', 'home/list/index/$1'], // list 或 list/123 67 | ['/^archive\/(\d{4})\/(\d{1,2})?$/', 'home/archive/index/$1/$2'], // archive/2023 或 archive/2023/12 68 | 69 | // 5. 复杂路径匹配 70 | ['/^([^\/]+)\/([^\/]+)\/(.+)$/', 'home/$1/$2/$3'], // 三级路径映射 71 | ['/^([^\/]+)\/(\d+)$/', 'home/$1/index/$2'], // 控制器/数字ID 72 | ['/^([^\/]+)$/', 'home/$1/index'], // 单级路径映射 73 | 74 | // 6. 特殊格式匹配 75 | ['/^api\/v(\d+)\/([^\/]+)$/', 'api/v$1/$2'], // api/v1/users -> api/v1/users 76 | ['/^(\d+)(.*?)$/', 'home/goods/index/$1'], // 数字开头的路径 77 | ['/^download\/([^\/]+)\.([a-z]+)$/', 'home/download/file/$1/$2'], // download/file.pdf 78 | 79 | 80 | */ 81 | 82 | return [ 83 | 84 | // 文章系统 85 | // ['article', 'home/article/index'], // 文章列表 86 | // ['article/(:num)', 'home/article/detail/$1'], // 文章详情 87 | // ['article/add', 'home/article/add'], // 添加文章 88 | // ['article/edit/(:num)', 'home/article/edit/$1'], // 编辑文章 89 | 90 | // 用户系统 91 | // ['login', 'home/user/login'], // 登录页面 92 | // ['register', 'home/user/register'], // 注册页面 93 | // ['profile/(:num)', 'home/user/profile/$1'], // 用户资料 94 | 95 | // 商品系统 96 | // ['goods', 'home/goods/index'], // 商品列表 97 | // ['goods/(:num)', 'home/goods/detail/$1'], // 商品详情 98 | // ['cart', 'home/cart/index'], // 购物车 99 | // ['order/(:num)', 'home/order/detail/$1'], // 订单详情 100 | ]; -------------------------------------------------------------------------------- /startmvc/core/Csrf.php: -------------------------------------------------------------------------------- 1 | $tokenLifetime)) { 126 | self::unsetToken(); 127 | return false; 128 | } 129 | 130 | // 使用 hash_equals 防止时序攻击 131 | if (!hash_equals($sessionToken, $postToken)) { 132 | return false; 133 | } 134 | 135 | // 确定是否删除 token(优先使用参数,其次使用配置) 136 | $shouldDelete = $deleteAfterCheck ?? self::getAutoDelete(); 137 | if ($shouldDelete) { 138 | self::unsetToken(); 139 | } 140 | 141 | return true; 142 | } 143 | 144 | /** 145 | * 删除 CSRF Token 146 | */ 147 | public static function unsetToken() 148 | { 149 | $tokenName = self::getTokenName(); 150 | Session::delete($tokenName); 151 | Session::delete($tokenName . '_time'); 152 | Session::delete($tokenName . '_lifetime'); 153 | } 154 | 155 | /** 156 | * 刷新 Token(生成新的 token) 157 | * @param int|null $lifetime 自定义有效期(秒) 158 | * @return string 159 | */ 160 | public static function refresh($lifetime = null) 161 | { 162 | return self::token(true, $lifetime); 163 | } 164 | 165 | /** 166 | * 获取当前 Token 的剩余有效时间(秒) 167 | * @return int|null 剩余秒数,如果 token 不存在则返回 null 168 | */ 169 | public static function getTokenTTL() 170 | { 171 | $tokenName = self::getTokenName(); 172 | $tokenTimeName = $tokenName . '_time'; 173 | $tokenLifetimeName = $tokenName . '_lifetime'; 174 | 175 | $tokenTime = Session::get($tokenTimeName); 176 | if (!$tokenTime) { 177 | return null; 178 | } 179 | 180 | $tokenLifetime = Session::get($tokenLifetimeName) ?? self::getTokenLifetime(); 181 | $elapsed = time() - $tokenTime; 182 | $remaining = $tokenLifetime - $elapsed; 183 | 184 | return max(0, $remaining); 185 | } 186 | } -------------------------------------------------------------------------------- /startmvc/core/Request.php: -------------------------------------------------------------------------------- 1 | $value) { 48 | if (strtolower($headerKey) === $key) { 49 | return $value; 50 | } 51 | } 52 | return $default; 53 | } 54 | return $headers; 55 | } 56 | 57 | /** 58 | * 判断是否为AJAX请求(静态方法) 59 | * @return bool 60 | */ 61 | public static function isAjax() 62 | { 63 | return self::header('X-Requested-With') === 'XMLHttpRequest'; 64 | } 65 | 66 | /** 67 | * 获取GET参数 68 | * @param string $key 键名 69 | * @param array $options 处理选项 70 | * @return mixed 71 | */ 72 | public static function get($key, $options = []) 73 | { 74 | $val = isset($_GET[$key]) ? $_GET[$key] : null; 75 | return Http::handling($val, $options); 76 | } 77 | 78 | /** 79 | * 获取POST参数 80 | * @param string $key 键名(为空则返回所有POST数据) 81 | * @param array $options 处理选项 82 | * @return mixed 83 | */ 84 | public static function post($key = '', $options = []) 85 | { 86 | $val = isset($_POST[$key]) ? $_POST[$key] : ($_POST ?: null); 87 | return Http::handling($val, $options); 88 | } 89 | 90 | /** 91 | * 获取原始POST输入 92 | * @return string 93 | */ 94 | public static function postInput() 95 | { 96 | return file_get_contents('php://input'); 97 | } 98 | 99 | /** 100 | * 获取JSON格式的POST数据 101 | * @param bool $assoc 是否转换为关联数组 102 | * @return mixed 103 | */ 104 | public static function getJson($assoc = true) 105 | { 106 | return json_decode(self::postInput(), $assoc); 107 | } 108 | 109 | /** 110 | * 获取所有请求头 111 | * @return array 112 | */ 113 | public static function headers() 114 | { 115 | $headers = []; 116 | foreach ($_SERVER as $key => $value) { 117 | if ('HTTP_' == substr($key, 0, 5)) { 118 | $headers[ucfirst(strtolower(str_replace('_', '-', substr($key, 5))))] = $value; 119 | } 120 | } 121 | return $headers; 122 | } 123 | 124 | /** 125 | * 获取请求方法 126 | * @return string 127 | */ 128 | public static function method() 129 | { 130 | return strtoupper($_SERVER['REQUEST_METHOD']); 131 | } 132 | 133 | /** 134 | * 判断是否为GET请求 135 | * @return bool 136 | */ 137 | public static function isGet() 138 | { 139 | return self::method() === 'GET'; 140 | } 141 | 142 | /** 143 | * 判断是否为POST请求 144 | * @return bool 145 | */ 146 | public static function isPost() 147 | { 148 | return self::method() === 'POST'; 149 | } 150 | 151 | /** 152 | * 判断是否为HTTPS请求 153 | * @return bool 154 | */ 155 | public static function isHttps() 156 | { 157 | return isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1) 158 | || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'; 159 | } 160 | 161 | /** 162 | * 获取客户端IP地址 163 | * @return string 164 | */ 165 | public static function ip() 166 | { 167 | $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; 168 | 169 | if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $match)) { 170 | foreach ($match[0] as $xip) { 171 | if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) { 172 | $ip = $xip; 173 | break; 174 | } 175 | } 176 | } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { 177 | $ip = $_SERVER['HTTP_CLIENT_IP']; 178 | } 179 | 180 | return $ip; 181 | } 182 | } -------------------------------------------------------------------------------- /startmvc/core/Session.php: -------------------------------------------------------------------------------- 1 | $value) { 116 | // 只处理带前缀且非空的键 117 | if ($prefix !== '' && strpos($key, $prefix) === 0) { 118 | $newKey = $withPrefix ? $key : substr($key, strlen($prefix)); 119 | $result[$newKey] = $value; 120 | } 121 | } 122 | 123 | return $result; 124 | } 125 | 126 | /** 127 | * 启动会话(确保会话已初始化) 128 | * @return void 129 | */ 130 | public static function start() 131 | { 132 | if (session_status() === PHP_SESSION_NONE) { 133 | // 加载会话安全配置 134 | $config = Config::load('common'); 135 | $sessionConfig = $config['session'] ?? []; 136 | 137 | // 应用会话配置 138 | if (!empty($sessionConfig)) { 139 | // 设置会话cookie参数 140 | if (isset($sessionConfig['cookie_lifetime'])) { 141 | session_set_cookie_params( 142 | $sessionConfig['cookie_lifetime'], // lifetime 143 | '/', // path 144 | '', // domain 145 | false, // secure 146 | $sessionConfig['cookie_httponly'] ?? true // httponly 147 | ); 148 | } 149 | 150 | // 设置其他会话选项 151 | foreach ($sessionConfig as $option => $value) { 152 | if ($option !== 'cookie_lifetime' && $option !== 'cookie_httponly') { 153 | $iniOption = 'session.' . $option; 154 | ini_set($iniOption, $value); 155 | } 156 | } 157 | } 158 | 159 | // 启动会话 160 | session_start(); 161 | 162 | // 防止会话固定攻击,定期更新会话ID (每30分钟) 163 | if (!isset($_SESSION['_last_regenerate']) || 164 | (time() - $_SESSION['_last_regenerate'] > 1800)) { 165 | session_regenerate_id(true); 166 | $_SESSION['_last_regenerate'] = time(); 167 | } 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /startmvc/core/Config.php: -------------------------------------------------------------------------------- 1 | $value) { 52 | if (isset(self::$config['common'][$key]) && is_array(self::$config['common'][$key]) && is_array($value)) { 53 | self::$config['common'][$key] = array_merge(self::$config['common'][$key], $value); 54 | } else { 55 | self::$config['common'][$key] = $value; 56 | } 57 | } 58 | } 59 | 60 | // 加载本地配置(如果存在) 61 | if (file_exists(CONFIG_PATH . 'local.php')) { 62 | $localConfig = require CONFIG_PATH . 'local.php'; 63 | foreach ($localConfig as $key => $value) { 64 | if (isset(self::$config['common'][$key]) && is_array(self::$config['common'][$key]) && is_array($value)) { 65 | self::$config['common'][$key] = array_merge(self::$config['common'][$key], $value); 66 | } else { 67 | self::$config['common'][$key] = $value; 68 | } 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * 获取配置项 75 | * @param string $key 配置键,支持file.key.subkey格式 76 | * @param mixed $default 默认值 77 | * @return mixed 78 | */ 79 | public static function get($key = null, $default = null) 80 | { 81 | // 确保初始化 82 | self::initialize(); 83 | 84 | // 不带参数时返回所有配置 85 | if ($key === null) { 86 | return self::$config; 87 | } 88 | 89 | // 处理点语法 90 | if (strpos($key, '.') !== false) { 91 | $parts = explode('.', $key); 92 | $config = self::$config; 93 | 94 | foreach ($parts as $part) { 95 | if (!is_array($config) || !array_key_exists($part, $config)) { 96 | return $default; 97 | } 98 | $config = $config[$part]; 99 | } 100 | 101 | return $config; 102 | } 103 | 104 | // 简单键名:先从 common 配置组中查找,再从其他配置组查找 105 | if (isset(self::$config['common'][$key])) { 106 | return self::$config['common'][$key]; 107 | } 108 | 109 | // 如果 common 中没有,再从其他配置组中查找 110 | return self::$config[$key] ?? $default; 111 | } 112 | 113 | /** 114 | * 设置配置项 115 | * @param string $key 配置键 116 | * @param mixed $value 配置值 117 | * @return bool 118 | */ 119 | public static function set($key, $value) 120 | { 121 | // 确保初始化 122 | self::initialize(); 123 | 124 | if (strpos($key, '.') !== false) { 125 | $parts = explode('.', $key); 126 | $config = &self::$config; 127 | 128 | // 逐级设置配置项 129 | $count = count($parts); 130 | foreach ($parts as $i => $part) { 131 | if ($i === $count - 1) { 132 | $config[$part] = $value; 133 | } else { 134 | if (!isset($config[$part]) || !is_array($config[$part])) { 135 | $config[$part] = []; 136 | } 137 | $config = &$config[$part]; 138 | } 139 | } 140 | } else { 141 | // 简单键名直接设置 142 | self::$config[$key] = $value; 143 | } 144 | 145 | return true; 146 | } 147 | 148 | /** 149 | * 检查配置项是否存在 150 | * @param string $key 配置键 151 | * @return bool 152 | */ 153 | public static function has($key) 154 | { 155 | return self::get($key) !== null; 156 | } 157 | 158 | /** 159 | * 加载指定配置文件 160 | * @param string $file 配置文件名(不含扩展名) 161 | * @return array|null 配置数据 162 | */ 163 | public static function load($file) 164 | { 165 | // 确保初始化 166 | self::initialize(); 167 | 168 | $filePath = CONFIG_PATH . $file . '.php'; 169 | 170 | if (!file_exists($filePath)) { 171 | return null; 172 | } 173 | 174 | // 如果配置已经加载过,直接返回 175 | if (isset(self::$config[$file])) { 176 | return self::$config[$file]; 177 | } 178 | 179 | // 加载配置文件 180 | self::$config[$file] = require $filePath; 181 | 182 | return self::$config[$file]; 183 | } 184 | 185 | /** 186 | * 获取配置分组 187 | * @param string $group 配置分组名 188 | * @return array 189 | */ 190 | public static function group($group) 191 | { 192 | // 确保初始化 193 | self::initialize(); 194 | 195 | // 如果存在完整的配置组,直接返回 196 | if (isset(self::$config[$group]) && is_array(self::$config[$group])) { 197 | return self::$config[$group]; 198 | } 199 | 200 | // 否则查找所有以 $group. 开头的配置项 201 | $result = []; 202 | foreach (self::$config as $configKey => $configValue) { 203 | if (is_array($configValue)) { 204 | foreach ($configValue as $key => $value) { 205 | if ($configKey === $group || strpos($key, $group . '.') === 0) { 206 | if ($configKey === $group) { 207 | $result[$key] = $value; 208 | } else { 209 | $subKey = substr($key, strlen($group) + 1); 210 | $result[$subKey] = $value; 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | return $result; 218 | } 219 | } -------------------------------------------------------------------------------- /startmvc/core/App.php: -------------------------------------------------------------------------------- 1 | registerMiddleware(); 24 | } 25 | public function run() 26 | { 27 | // 记录开始时间和内存 28 | $beginTime = microtime(true); 29 | $beginMem = memory_get_usage(); 30 | 31 | // 初始化 trace 数据 32 | self::$trace = [ 33 | 'beginTime' => $beginTime, 34 | 'beginMem' => $beginMem, 35 | 'uri' => $_SERVER['REQUEST_URI'], 36 | 'request_method' => $_SERVER['REQUEST_METHOD'] 37 | ]; 38 | 39 | try { 40 | Exception::init(); 41 | $this->loadFunction(); 42 | 43 | // 创建请求对象 44 | $request = new Request(); 45 | 46 | // 通过中间件管道处理请求 47 | $response = Middleware::run($this, function() { 48 | return $this->handleRequest(); 49 | }); 50 | 51 | // 输出响应内容 52 | if (is_string($response)) { 53 | echo $response; 54 | // 对于字符串响应,在末尾添加 trace 信息 55 | if (config('trace')) { 56 | $this->outputTrace(); 57 | } 58 | } elseif (is_array($response)) { 59 | header('Content-Type: application/json'); 60 | echo json_encode($response); 61 | } 62 | // 注意:如果 $response 为 null(控制器直接输出了内容),trace 会在 Controller::display 中处理 63 | 64 | } catch (\Exception $e) { 65 | throw $e; 66 | } 67 | } 68 | 69 | /** 70 | * 输出 trace 信息 71 | */ 72 | public static function outputTrace() 73 | { 74 | // 记录结束时间和内存 75 | $endTime = microtime(true); 76 | $endMem = memory_get_usage(); 77 | 78 | // 计算运行时间和内存使用 79 | self::$trace['endTime'] = $endTime; 80 | self::$trace['endMem'] = $endMem; 81 | self::$trace['runtime'] = number_format((self::$trace['endTime'] - self::$trace['beginTime']) * 1000, 2) . 'ms'; 82 | self::$trace['memory'] = number_format((self::$trace['endMem'] - self::$trace['beginMem']) / 1024, 2) . 'KB'; 83 | self::$trace['files'] = get_included_files(); 84 | 85 | echo "\n\n"; 86 | include __DIR__ . '/tpl/trace.php'; 87 | echo "\n\n"; 88 | } 89 | 90 | /** 91 | * 加载自定义函数 92 | */ 93 | private static function loadFunction($dirPath = ROOT_PATH.'function'.DS.'*.php') 94 | { 95 | $files=glob($dirPath); 96 | if (is_array($files)) { 97 | foreach ($files as $v) { 98 | if(is_file($v)) require_once($v); 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * 配置控制器的路径 105 | */ 106 | private static function startApp($module, $controller, $action, $argv) 107 | { 108 | // 先定义常量,因为 View 类的构造函数需要用到 109 | if (!defined('MODULE')) define('MODULE', $module); 110 | if (!defined('CONTROLLER')) define('CONTROLLER', $controller); 111 | if (!defined('ACTION')) define('ACTION', $action); 112 | 113 | $controller = APP_NAMESPACE . "\\{$module}\\controller\\{$controller}Controller"; 114 | if (!class_exists($controller)) { 115 | throw new \Exception($controller.'控制器不存在'); 116 | } 117 | $action .= 'Action'; 118 | return Loader::make($controller, $action, $argv); 119 | } 120 | /** 121 | * 自定义错误处理触发错误 122 | */ 123 | public static function errorHandler($level,$message, $file, $line) 124 | { 125 | if (error_reporting() !== 0) { 126 | $errorMessage = "错误提示:{$message},文件:{$file},行号:{$line}"; 127 | throw new \Exception($errorMessage, $level); 128 | } 129 | } 130 | /** 131 | * 异常错误处理 132 | */ 133 | public static function exceptionHandler($exception) 134 | { 135 | // Code is 404 (not found) or 500 (general error) 136 | $code = $exception->getCode(); 137 | if ($code != 404) { 138 | $code = 500; 139 | } 140 | http_response_code($code); 141 | if (config('debug')) { 142 | include 'tpl/debug.php'; 143 | //var_dump($exception); 144 | } else { 145 | //$log = new Log(); 146 | //$log->debug($exception->getMessage() . '\n' . $exception->getFile() . '\n' . $exception->getLine()); 147 | return $code; 148 | } 149 | } 150 | 151 | /** 152 | * 注册默认中间件 153 | */ 154 | protected function registerMiddleware() 155 | { 156 | // 从配置文件加载中间件 157 | $middleware = config('middleware') ?? []; 158 | 159 | // 注册中间件别名 160 | $aliases = $middleware['aliases'] ?? []; 161 | foreach ($aliases as $alias => $class) { 162 | Middleware::alias($alias, $class); 163 | } 164 | 165 | // 注册全局中间件 166 | $global = $middleware['global'] ?? []; 167 | foreach ($global as $middlewareClass) { 168 | Middleware::register($middlewareClass); 169 | } 170 | } 171 | 172 | /** 173 | * 处理请求 174 | */ 175 | private function handleRequest() 176 | { 177 | try { 178 | // 获取当前URI 179 | $uri = $_SERVER['REQUEST_URI']; 180 | 181 | // 移除查询字符串 182 | if (strpos($uri, '?') !== false) { 183 | $uri = substr($uri, 0, strpos($uri, '?')); 184 | } 185 | 186 | // 移除前后的斜杠 187 | $uri = trim($uri, '/'); 188 | 189 | // 过滤入口文件名(如index.php) 190 | $scriptName = basename($_SERVER['SCRIPT_NAME']); 191 | if (strpos($uri, $scriptName) === 0) { 192 | $uri = substr($uri, strlen($scriptName)); 193 | $uri = trim($uri, '/'); 194 | } 195 | 196 | // 使用Router类的parse方法解析URI(Router会自动处理URL后缀) 197 | $parseResult = Router::parse($uri); 198 | 199 | if ($parseResult && count($parseResult) >= 3) { 200 | $module = $parseResult[0]; 201 | $controller = $parseResult[1]; 202 | $action = $parseResult[2]; 203 | $params = isset($parseResult[3]) ? $parseResult[3] : []; 204 | } else { 205 | // 如果解析失败,使用默认值 206 | $module = Config::get('common.default_module', 'home'); 207 | $controller = Config::get('common.default_controller', 'Index'); 208 | $action = Config::get('common.default_action', 'index'); 209 | $params = []; 210 | } 211 | 212 | // 使用原有的startApp方法 213 | return self::startApp($module, $controller, $action, $params); 214 | 215 | } catch (\Exception $e) { 216 | throw $e; 217 | } 218 | } 219 | 220 | /** 221 | * 显示追踪信息 222 | */ 223 | protected static function showTrace() 224 | { 225 | // 确保输出在页面最后 226 | register_shutdown_function(function() { 227 | // 包含trace模板 228 | include __DIR__ . '/tpl/trace.php'; 229 | }); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /startmvc/function.php: -------------------------------------------------------------------------------- 1 | \n(\s+)/m", "] => ", $output); 65 | 66 | $cli = preg_match("/cli/i", PHP_SAPI) ? true : false; 67 | 68 | if ($cli === true) { 69 | $output = PHP_EOL . $label . PHP_EOL . $output . PHP_EOL; 70 | } else { 71 | $output = '
' . PHP_EOL . $label . PHP_EOL . $output . '
' . PHP_EOL; 72 | } 73 | 74 | if ($echo) { 75 | echo $output; 76 | } 77 | 78 | return $output; 79 | } 80 | 81 | /** 82 | * 配置文件函数 83 | * @param string|array $key 配置键名或配置数组 84 | * @param mixed $value 配置值,不提供则为获取配置 85 | * @return mixed 86 | */ 87 | function config($key = null, $value = null) 88 | { 89 | // 获取所有配置 90 | if ($key === null) { 91 | return \startmvc\core\Config::get(); 92 | } 93 | 94 | // 加载配置文件 95 | if (is_string($key) && strpos($key, '@') === 0) { 96 | return \startmvc\core\Config::load(substr($key, 1)); 97 | } 98 | 99 | // 设置多个配置 100 | if (is_array($key)) { 101 | foreach ($key as $k => $v) { 102 | \startmvc\core\Config::set($k, $v); 103 | } 104 | return true; 105 | } 106 | 107 | // 设置单个配置 108 | if (func_num_args() > 1) { 109 | return \startmvc\core\Config::set($key, $value); 110 | } 111 | 112 | // 获取配置 113 | return \startmvc\core\Config::get($key); 114 | } 115 | 116 | /** 117 | * 缓存助手函数 118 | * 119 | * @param string $name 缓存名称(注意命名唯一性,防止重复) 120 | * @param mixed $value 缓存值,为null时表示获取缓存 121 | * @param int $expire 缓存时间(秒),默认3600秒 122 | * @param string $driver 缓存驱动,默认使用配置中的驱动 123 | * @return mixed 获取缓存时返回缓存值,设置缓存时返回true/false 124 | */ 125 | function cache($name, $value = null, $expire = 3600, $driver = null) 126 | { 127 | static $instance = []; 128 | 129 | // 获取缓存驱动实例 130 | $driverName = $driver ?: config('cache.drive', 'file'); 131 | if (!isset($instance[$driverName])) { 132 | $instance[$driverName] = Cache::store($driverName); 133 | } 134 | 135 | // 获取缓存 136 | if ($value === null) { 137 | return $instance[$driverName]->get($name); 138 | } 139 | 140 | // 删除缓存 141 | if ($value === false) { 142 | return $instance[$driverName]->delete($name); 143 | } 144 | 145 | // 自定义缓存参数 146 | $cacheConfig = config('cache.' . $driverName, []); 147 | if ($expire !== 3600) { 148 | $cacheConfig['cacheTime'] = $expire; 149 | } 150 | 151 | // 设置缓存 152 | return $instance[$driverName]->set($name, $value); 153 | } 154 | 155 | /** 156 | * url的方法 157 | */ 158 | function url($url){ 159 | $url = ltrim($url, '/'); 160 | $url = $url . config('url_suffix'); 161 | if (config('urlrewrite')) { 162 | $url = '/' . $url; 163 | } else { 164 | $url = '/index.php/' . $url; 165 | } 166 | return str_replace('%2F', '/', urlencode($url)); 167 | } 168 | 169 | /** 170 | * 数据库助手函数 - 支持链式操作和自定义配置 171 | * 172 | * 使用示例: 173 | * db('user')->where('uid', 1)->get() // 使用默认配置 174 | * db('user', $config)->where('uid', 1)->get() // 使用自定义配置 175 | * db()->table('user')->where('uid', 1)->get() // 链式调用 176 | * 177 | * 更多示例: 178 | * db('user')->where('status', 1)->select('id,name')->getAll() 179 | * db('user')->insert(['name' => 'test', 'email' => 'test@example.com']) 180 | * db('user')->where('id', 1)->update(['name' => 'updated']) 181 | * db('user')->where('id', 1)->delete() 182 | * 183 | * @param string $table 表名 184 | * @param array $config 数据库配置(可选) 185 | * @return \startmvc\core\db\DbCore 186 | */ 187 | function db($table = '', $config = []) 188 | { 189 | // 如果指定了表名,直接调用Db::connect()方法 190 | if (!empty($table)) { 191 | return Db::connect($config, $table); 192 | } 193 | 194 | // 如果没有指定表名,返回Db门面类的代理对象以支持其他静态方法调用 195 | return new class($config) { 196 | private $config; 197 | 198 | public function __construct($config = []) { 199 | $this->config = $config; 200 | } 201 | 202 | public function __call($method, $args) { 203 | // 对于table方法,传入配置参数 204 | if ($method === 'table' && !empty($this->config)) { 205 | return Db::table($args[0], $this->config); 206 | } 207 | return call_user_func_array([Db::class, $method], $args); 208 | } 209 | }; 210 | } 211 | 212 | 213 | 214 | /** 215 | * 获取客户端的真实IP地址 216 | */ 217 | function get_ip() { 218 | // 优先检查HTTP_X_FORWARDED_FOR,因为它可能包含多个IP,我们取第一个非未知的IP 219 | $ip = null; 220 | if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { 221 | $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); 222 | foreach ($ips as $tmp) { 223 | $ip = trim($tmp); 224 | if ($ip !== 'unknown') { 225 | break; 226 | } 227 | } 228 | } 229 | 230 | // 如果没有通过HTTP_X_FORWARDED_FOR获取到IP,尝试其他可能的服务器变量 231 | if (!$ip) { 232 | $ip = $_SERVER['REMOTE_ADDR'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['HTTP_CDN_SRC_IP'] ?? '0.0.0.0'; 233 | } 234 | 235 | // 验证IP地址格式,如果不是有效的IPv4或IPv6,返回默认值 236 | if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) { 237 | $ip = '0.0.0.0'; 238 | } 239 | 240 | return $ip; 241 | } -------------------------------------------------------------------------------- /startmvc/core/Model.php: -------------------------------------------------------------------------------- 1 | dbConf = include CONFIG_PATH . '/database.php'; 48 | } 49 | 50 | /** 51 | * 设置表名 52 | * 53 | * @param string $table 表名 54 | * @return $this 55 | */ 56 | public function table($table) 57 | { 58 | $this->table = $table; 59 | return $this; 60 | } 61 | 62 | /** 63 | * 设置模型数据 64 | * 65 | * @param array $data 数据 66 | * @return $this 67 | */ 68 | public function data($data) 69 | { 70 | $this->data = array_merge($this->data, $data); 71 | return $this; 72 | } 73 | 74 | /** 75 | * 插入数据 76 | * 77 | * @param array $data 数据 78 | * @return int|bool 插入ID或结果 79 | */ 80 | public function insert($data = []) 81 | { 82 | if (!empty($data)) { 83 | $this->data = $data; 84 | } 85 | 86 | return Db::table($this->table)->insert($this->data); 87 | } 88 | 89 | /** 90 | * 更新数据 91 | * 92 | * @param array $data 要更新的数据 93 | * @param mixed $where 条件(数组、字符串或整数id) 94 | * @return int|bool 影响行数或结果 95 | */ 96 | public function update($data, $where = []) 97 | { 98 | if (!empty($data)) { 99 | $this->data = $data; 100 | } 101 | 102 | $query = Db::table($this->table); 103 | 104 | if (!empty($where)) { 105 | if (is_numeric($where)) { 106 | // 如果是纯数字,认为是按主键查询 107 | $query->where($this->pk, $where); 108 | } elseif (is_array($where)) { 109 | // 如果是数组,则按条件数组处理 110 | $query->where($where); 111 | } elseif (is_string($where)) { 112 | // 如果是字符串,判断是否为条件表达式 113 | if (preg_match('/[=<>!]/', $where)) { 114 | // 包含运算符,视为条件表达式 115 | $query->where($where); 116 | } else { 117 | // 不包含运算符,视为主键值 118 | $query->where($this->pk, $where); 119 | } 120 | } 121 | } 122 | 123 | return $query->update($this->data); 124 | } 125 | 126 | /** 127 | * 保存数据(自动判断插入或更新) 128 | * 129 | * @param array $data 数据 130 | * @return int|bool 结果 131 | */ 132 | public function save($data = []) 133 | { 134 | if (!empty($data)) { 135 | $this->data = $data; 136 | } 137 | 138 | if (isset($this->data[$this->pk]) && !empty($this->data[$this->pk])) { 139 | // 有主键,执行更新 140 | $id = $this->data[$this->pk]; 141 | $updateData = $this->data; 142 | return $this->update($updateData, $id); 143 | } else { 144 | // 无主键,执行插入 145 | return $this->insert(); 146 | } 147 | } 148 | 149 | /** 150 | * 删除数据 151 | * 152 | * @param mixed $where 条件(数组、字符串或整数id) 153 | * @return int|bool 影响行数或结果 154 | */ 155 | public function delete($where = null) 156 | { 157 | $query = Db::table($this->table); 158 | 159 | if ($where !== null) { 160 | if (is_numeric($where)) { 161 | // 数字条件转为主键条件 162 | $query->where($this->pk, $where); 163 | } else { 164 | $query->where($where); 165 | } 166 | } 167 | 168 | return $query->delete(); 169 | } 170 | 171 | /** 172 | * 魔术方法:调用不存在的方法时自动调用db对象的方法 173 | * 174 | * @param string $method 方法名 175 | * @param array $args 参数 176 | * @return mixed 返回结果 177 | */ 178 | public function __call($method, $args) 179 | { 180 | $query = Db::table($this->table); 181 | 182 | if (method_exists($query, $method)) { 183 | return call_user_func_array([$query, $method], $args); 184 | } 185 | 186 | throw new \Exception("方法 {$method} 不存在"); 187 | } 188 | 189 | /** 190 | * 查找单条记录 191 | * 192 | * @param mixed $where 查询条件(主键值、条件数组或字符串条件表达式) 193 | * @param string|array $fields 查询字段,默认为* 194 | * @return array|null 返回符合条件的单条记录 195 | */ 196 | public function find($where, $fields = '*') 197 | { 198 | $query = Db::table($this->table); 199 | 200 | // 设置查询字段 201 | $query->select($fields); 202 | 203 | // 处理查询条件 204 | if (is_numeric($where)) { 205 | // 如果是纯数字,认为是按主键查询 206 | $query->where($this->pk, $where); 207 | } elseif (is_array($where)) { 208 | // 如果是数组,则按条件数组处理 209 | $query->where($where); 210 | } elseif (is_string($where)) { 211 | // 如果是字符串,判断是否为条件表达式 212 | if (preg_match('/[=<>!]/', $where)) { 213 | // 包含运算符,视为条件表达式 214 | $query->where($where); 215 | } else { 216 | // 不包含运算符,视为主键值 217 | $query->where($this->pk, $where); 218 | } 219 | } 220 | 221 | // 执行查询并返回单条记录 222 | return $query->first(); 223 | } 224 | 225 | 226 | /** 227 | * 查找多条记录 228 | * 229 | * @param mixed $where 查询条件(条件数组或字符串条件表达式) 230 | * @param string|array $fields 查询字段,默认为* 231 | * @param string|array $order 排序方式 232 | * @param int|string $limit 查询限制 233 | * @return array 返回符合条件的记录集 234 | */ 235 | public function findAll($where = [], $fields = '*', $order = '', $limit = '') 236 | { 237 | $query = Db::table($this->table); 238 | 239 | // 设置查询字段 240 | $query->select($fields); 241 | 242 | // 处理查询条件 243 | if (!empty($where)) { 244 | if (is_array($where)) { 245 | $query->where($where); 246 | } elseif (is_string($where)) { 247 | // 字符串条件 248 | $query->where($where); 249 | } 250 | } 251 | 252 | // 设置排序 253 | if (!empty($order)) { 254 | if (is_array($order)) { 255 | foreach ($order as $field => $sort) { 256 | if (is_numeric($field)) { 257 | $query->order($sort); 258 | } else { 259 | $query->order($field, $sort); 260 | } 261 | } 262 | } else { 263 | $query->order($order); 264 | } 265 | } 266 | 267 | // 设置查询限制 268 | if (!empty($limit)) { 269 | if (is_numeric($limit)) { 270 | $query->limit($limit); 271 | } elseif (is_string($limit) && strpos($limit, ',') !== false) { 272 | list($offset, $rows) = explode(',', $limit); 273 | $query->limit($rows, $offset); 274 | } 275 | } 276 | 277 | // 执行查询 278 | return $query->get(); 279 | } 280 | 281 | /** 282 | * 静态方法:实例化模型 283 | * 284 | * @param string $table 表名 285 | * @return static 模型实例 286 | */ 287 | public static function model($table = null) 288 | { 289 | $model = new static(); 290 | 291 | if ($table !== null) { 292 | $model->table($table); 293 | } 294 | 295 | return $model; 296 | } 297 | 298 | /** 299 | * 分页查询方法 300 | * 301 | * @param int $pageSize 每页记录数 302 | * @param int $currentPage 当前页码 303 | * @param mixed $where 查询条件 304 | * @param string $order 排序方式 305 | * @return array 包含数据和分页信息的数组 306 | */ 307 | public function paginate($pageSize = 10, $currentPage = 1, $where = [], $order = '') 308 | { 309 | // 查询总记录数 310 | $countQuery = Db::table($this->table); 311 | if (!empty($where)) { 312 | if (is_array($where)) { 313 | $countQuery->where($where); 314 | } elseif (is_string($where)) { 315 | $countQuery->where($where); 316 | } 317 | } 318 | $total = $countQuery->count(); 319 | 320 | // 计算总页数 321 | $totalPages = ceil($total / $pageSize); 322 | 323 | // 确保当前页码有效 324 | $currentPage = max(1, min($totalPages, $currentPage)); 325 | 326 | // 查询当前页数据 327 | $query = Db::table($this->table); 328 | 329 | // 处理查询条件 330 | if (!empty($where)) { 331 | if (is_array($where)) { 332 | $query->where($where); 333 | } elseif (is_string($where)) { 334 | $query->where($where); 335 | } 336 | } 337 | 338 | // 设置排序 339 | if (!empty($order)) { 340 | $query->order($order); 341 | } 342 | 343 | // 设置分页 344 | $query->page($pageSize, $currentPage); 345 | 346 | // 执行查询 347 | $data = $query->get(); 348 | 349 | // 返回分页数据 350 | return [ 351 | 'data' => $data, 352 | 'pagination' => [ 353 | 'total' => $total, 354 | 'per_page' => $pageSize, 355 | 'current_page' => $currentPage, 356 | 'total_pages' => $totalPages, 357 | 'has_more' => $currentPage < $totalPages 358 | ] 359 | ]; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /startmvc/core/View.php: -------------------------------------------------------------------------------- 1 | '', 33 | 34 | '/{\$([^\}|\.]{1,})}/i' => '', 35 | 36 | // array: {$array.key} 37 | '/{\$([0-9a-z_]{1,})\.([0-9a-z_]{1,})}/i' => '', 38 | 39 | // two-demensional array 40 | '/{\$([0-9a-z_]{1,})\.([0-9a-z_]{1,})\.([0-9a-z_]{1,})}/i' => '', 41 | 42 | // for loop 43 | '/{for ([^\}]+)}/i' => '', 44 | '/{\/for}/i' => '', 45 | 46 | // foreach ( $array as $key => $value ) 47 | '/{loop\s+\$([^\}]{1,})\s+\$([^\}]{1,})\s+\$([^\}]{1,})\s*}/i' => ' $${3} ) { ?>', 48 | '/{\/loop}/i' => '', 49 | 50 | // foreach ( $array as $value ) 51 | '/{loop\s+\$(.*?)\s+\$([0-9a-z_]{1,})\s*}/i' => '', 52 | 53 | // foreach ( $array as $key => $value ) 54 | '/{foreach\s+(.*?)}/i' => '', 55 | //end foreach 56 | '/{\/foreach}/i' => '', 57 | 58 | // php: excute the php expression 59 | // echo: print the php expression 60 | '/{php\s+(.*?)}/i' => '', 61 | '/{echo\s+(.*?)}/i' => '', 62 | 63 | // if else tag 64 | '/{if\s+(.*?)}/i' => '', 65 | '/{else}/i' => '', 66 | '/{elseif\s+(.*?)}/i' => '', 67 | '/{\/if}/i' => '', 68 | 69 | //lang 70 | '/\{lang\(\'([^\']+)\'\)\}/'=>'', 71 | 72 | // require|include tag 73 | '/{include\s+([^}]+)\}/i'=> 'getInclude(\'${1}\');?>', 74 | 75 | // comment tag (不会被解析) 76 | '/{\/\*(.*?)\*\/}/s' => '', 77 | 78 | // 三元运算 79 | '/{\$([^\}|\.]{1,})\?(.*?):(.*?)}/i' => '', 80 | 81 | // 输出带HTML标签的内容 82 | '/{html\s+\$(.*?)}/i' => '', 83 | 84 | // 日期格式化 85 | '/{date\s+\$(.*?)\s+(.*?)}/i' => '', 86 | ]; 87 | 88 | function __construct(){ 89 | // 使用常量或默认值 90 | $module = defined('MODULE') ? MODULE : 'home'; 91 | $controller = defined('CONTROLLER') ? CONTROLLER : 'Index'; 92 | $action = defined('ACTION') ? ACTION : 'index'; 93 | 94 | $theme=config('theme')?config('theme').DS:''; 95 | $this->tpl_template_dir = APP_PATH .MODULE . DS. 'view'.DS.$theme; 96 | $this->tpl_compile_dir = TEMP_PATH.MODULE.DS; 97 | $this->left_delimiter_quote = preg_quote($this->tpl_left_delimiter); 98 | $this->right_delimiter_quote = preg_quote($this->tpl_right_delimiter); 99 | 100 | // 读取配置的缓存时间 101 | $this->tpl_cache_time = intval(config('tpl_cache_time', 0)); 102 | 103 | // 读取模板后缀配置 104 | $viewConfig = Config::load('view'); 105 | if (isset($viewConfig['suffix']) && !empty($viewConfig['suffix'])) { 106 | $this->tpl_suffix = $viewConfig['suffix']; 107 | } 108 | } 109 | 110 | //模板赋值 111 | public function assign($name, $value='') { 112 | if (is_array($name)) { 113 | foreach ($name as $k => $v) { 114 | if ($k != '') { 115 | self::$vars[$k] = $v; // 使用静态属性 116 | } 117 | } 118 | } else { 119 | self::$vars[$name] = $value; // 使用静态属性 120 | } 121 | return $this; // 支持链式调用 122 | } 123 | 124 | /** 125 | * 获取模板文件路径和缓存文件路径 126 | * 127 | * @param string $name 模板名称 128 | * @return array 包含模板文件路径和缓存文件路径的数组 129 | */ 130 | protected function getTemplatePaths($name) { 131 | if ($name == '') { 132 | $name = strtolower(CONTROLLER . DS . ACTION); 133 | } 134 | 135 | // 检查是否已经包含文件扩展名 136 | $fileExtension = pathinfo($name, PATHINFO_EXTENSION); 137 | $hasExtension = !empty($fileExtension); 138 | 139 | // 基础路径(不包含扩展名) 140 | $baseName = $hasExtension ? substr($name, 0, strrpos($name, '.')) : $name; 141 | 142 | // 模板文件路径 143 | $tplFile = $this->tpl_template_dir . $baseName; 144 | if ($hasExtension) { 145 | $tplFile .= '.' . $fileExtension; 146 | } else { 147 | $tplFile .= $this->tpl_suffix; 148 | } 149 | 150 | // 缓存文件路径(始终使用.php后缀) 151 | $cacheFile = $this->tpl_compile_dir . $baseName . '.php'; 152 | 153 | return ['tplFile' => $tplFile, 'cacheFile' => $cacheFile]; 154 | } 155 | 156 | //视图渲染 支持多级目录 157 | public function display($name='', $data=[]) 158 | { 159 | $paths = $this->getTemplatePaths($name); 160 | $tplFile = $paths['tplFile']; 161 | $cacheFile = $paths['cacheFile']; 162 | 163 | // 模板文件不存在直接返回 164 | if (!file_exists($tplFile)) { 165 | throw new \Exception($tplFile.' 模板文件不存在'); 166 | } 167 | 168 | if (!empty($data)) { 169 | self::$vars = array_merge(self::$vars, $data); // 使用静态属性 170 | } 171 | // 将变量导入到当前 172 | extract(self::$vars); // 使用静态属性 173 | // 获取渲染后的内容 174 | ob_start(); 175 | $this->_compile($tplFile, $cacheFile); 176 | include $cacheFile; 177 | $content = ob_get_clean(); 178 | 179 | // 直接输出内容,不要处理trace 180 | echo $content; 181 | 182 | return $this; // 支持链式调用 183 | } 184 | 185 | // 返回渲染后的内容,而不是直接输出 186 | public function fetch($name='', $data=[]) 187 | { 188 | $paths = $this->getTemplatePaths($name); 189 | $tplFile = $paths['tplFile']; 190 | $cacheFile = $paths['cacheFile']; 191 | 192 | // 模板文件不存在直接返回 193 | if (!file_exists($tplFile)) { 194 | throw new \Exception($tplFile.' 模板文件不存在'); 195 | } 196 | 197 | if (!empty($data)) { 198 | self::$vars = array_merge(self::$vars, $data); // 使用静态属性 199 | } 200 | // 将变量导入到当前 201 | extract(self::$vars); // 使用静态属性 202 | // 获取渲染后的内容 203 | ob_start(); 204 | $this->_compile($tplFile, $cacheFile); 205 | include $cacheFile; 206 | return ob_get_clean(); 207 | } 208 | 209 | /** 210 | * compile template 211 | */ 212 | private function _compile($tplFile, $cacheFile) 213 | { 214 | $tplCacheDir = dirname($cacheFile); 215 | 216 | // 检查缓存是否有效 217 | if (file_exists($cacheFile)) { 218 | $cacheModified = filemtime($cacheFile); 219 | $tplModified = filemtime($tplFile); 220 | 221 | // 如果缓存未过期且模板未修改,直接使用缓存 222 | if ($this->tpl_cache_time > 0 && 223 | (time() - $cacheModified < $this->tpl_cache_time) && 224 | $tplModified <= $cacheModified) { 225 | return; 226 | } 227 | } 228 | 229 | // 编译模板 230 | $content = @file_get_contents($tplFile); 231 | if ($content === false) { 232 | throw new \Exception("无法加载模板文件 {$tplFile}"); 233 | } 234 | 235 | // 增加编译前的钩子,可以自定义修改模板内容 236 | if (method_exists($this, 'beforeCompile')) { 237 | $content = $this->beforeCompile($content); 238 | } 239 | 240 | // 处理include标签,将include的内容合并到主模板 241 | $content = $this->parseIncludeTags($content); 242 | 243 | // 执行模板标签替换 244 | $content = preg_replace(array_keys(self::$rules), self::$rules, $content); 245 | 246 | // 增加编译后的钩子 247 | if (method_exists($this, 'afterCompile')) { 248 | $content = $this->afterCompile($content); 249 | } 250 | 251 | // 确保缓存目录存在 252 | if (!is_dir($tplCacheDir)) { 253 | mkdir($tplCacheDir, 0777, true); 254 | } 255 | 256 | // 添加编译时间戳注释 257 | $content = "\n" . $content; 258 | 259 | file_put_contents($cacheFile, $content, LOCK_EX); 260 | } 261 | 262 | /** 263 | * 处理模板中的include标签,将被包含文件的内容合并到主模板中 264 | */ 265 | protected function parseIncludeTags($content) { 266 | return preg_replace_callback( 267 | '/{include\s+([^}]+)}/i', 268 | function($matches) { 269 | return $this->getIncludeContent($matches[1]); 270 | }, 271 | $content 272 | ); 273 | } 274 | 275 | /** 276 | * 获取被包含模板的内容(不执行,只返回内容) 277 | */ 278 | protected function getIncludeContent($name) { 279 | if (empty($name)) { 280 | return ''; 281 | } 282 | 283 | // 解析可能的参数 284 | $params = []; 285 | if (strpos($name, '?') !== false) { 286 | list($name, $query) = explode('?', $name, 2); 287 | parse_str($query, $params); 288 | } 289 | 290 | // 检查是否指定了模块 {include common/header|Admin} 291 | $tplFile = ''; 292 | if (strpos($name, '|') !== false) { 293 | list($path, $module) = explode('|', $name, 2); 294 | $module = trim($module); 295 | $path = trim($path); 296 | 297 | // 检查是否已经包含文件扩展名 298 | $fileExtension = pathinfo($path, PATHINFO_EXTENSION); 299 | 300 | // 构建跨模块模板路径 301 | $theme = config('theme') ? config('theme') . DS : ''; 302 | $moduleDir = APP_PATH . strtolower($module) . DS . 'view' . DS . $theme; 303 | 304 | if (!empty($fileExtension)) { 305 | $tplFile = $moduleDir . $path; 306 | } else { 307 | $tplFile = $moduleDir . $path . $this->tpl_suffix; 308 | } 309 | } else { 310 | // 使用当前模块 311 | $fileExtension = pathinfo($name, PATHINFO_EXTENSION); 312 | if (!empty($fileExtension)) { 313 | $tplFile = $this->tpl_template_dir . $name; 314 | } else { 315 | $tplFile = $this->tpl_template_dir . $name . $this->tpl_suffix; 316 | } 317 | } 318 | 319 | if (file_exists($tplFile)) { 320 | // 读取包含文件内容 321 | $content = file_get_contents($tplFile); 322 | 323 | // 递归处理嵌套的include标签 324 | $content = $this->parseIncludeTags($content); 325 | 326 | // 如果有参数,将参数作为变量添加到内容中 327 | if (!empty($params)) { 328 | $paramCode = ''; 329 | foreach ($params as $key => $value) { 330 | $paramCode .= ''; 331 | } 332 | $content = $paramCode . $content; 333 | } 334 | 335 | return $content; 336 | } 337 | 338 | return ''; 339 | } 340 | 341 | // 获取被包含模板的内容(用于运行时) 342 | public function getInclude($name = null) { 343 | if (empty($name)) { 344 | return ''; 345 | } 346 | 347 | // 解析可能的参数 348 | $params = []; 349 | if (strpos($name, '?') !== false) { 350 | list($name, $query) = explode('?', $name, 2); 351 | parse_str($query, $params); 352 | } 353 | // 检查是否指定了模块 {include common/header|Admin} 354 | $tplFile = ''; 355 | if (strpos($name, '|') !== false) { 356 | list($path, $module) = explode('|', $name, 2); 357 | $module = trim($module); 358 | $path = trim($path); 359 | 360 | // 检查是否已经包含文件扩展名 361 | $fileExtension = pathinfo($path, PATHINFO_EXTENSION); 362 | 363 | // 构建跨模块模板路径 364 | $theme = config('theme') ? config('theme') . DS : ''; 365 | $moduleDir = APP_PATH . strtolower($module) . DS . 'view' . DS . $theme; 366 | 367 | if (!empty($fileExtension)) { 368 | $tplFile = $moduleDir . $path; 369 | } else { 370 | $tplFile = $moduleDir . $path . $this->tpl_suffix; 371 | } 372 | } else { 373 | // 使用当前模块 374 | $fileExtension = pathinfo($name, PATHINFO_EXTENSION); 375 | if (!empty($fileExtension)) { 376 | $tplFile = $this->tpl_template_dir . $name; 377 | } else { 378 | $tplFile = $this->tpl_template_dir . $name . $this->tpl_suffix; 379 | } 380 | } 381 | 382 | if (file_exists($tplFile)) { 383 | // 读取包含文件内容 384 | $content = file_get_contents($tplFile); 385 | 386 | // 递归编译包含文件中的包含标签 387 | $content = $this->parseIncludeTags($content); 388 | 389 | // 编译其他模板标签 390 | $content = preg_replace(array_keys(self::$rules), self::$rules, $content); 391 | 392 | // 创建临时文件以执行 393 | $tempFile = $this->tpl_compile_dir . md5($name . microtime(true)) . '.php'; 394 | file_put_contents($tempFile, $content); 395 | 396 | // 合并当前变量和传递的参数 397 | $mergedVars = array_merge(self::$vars, $params); 398 | 399 | // 捕获输出 400 | ob_start(); 401 | extract($mergedVars); // 提取变量到当前作用域 402 | include $tempFile; 403 | $output = ob_get_clean(); 404 | 405 | // 清理临时文件 406 | @unlink($tempFile); 407 | 408 | return $output; 409 | } 410 | 411 | return ''; 412 | } 413 | 414 | // 清除模板缓存 415 | public function clearCache($name = null) { 416 | if ($name === null) { 417 | // 清除所有缓存 418 | $this->_clearDir($this->tpl_compile_dir); 419 | } else { 420 | // 清除指定模板缓存 421 | $cacheFile = $this->tpl_compile_dir . $name . '.php'; 422 | if (file_exists($cacheFile)) { 423 | @unlink($cacheFile); 424 | } 425 | } 426 | return $this; 427 | } 428 | 429 | // 清空目录 430 | private function _clearDir($dir) { 431 | if (!is_dir($dir)) return; 432 | 433 | $handle = opendir($dir); 434 | while (false !== ($file = readdir($handle))) { 435 | if ($file != '.' && $file != '..') { 436 | $path = $dir . $file; 437 | if (is_dir($path)) { 438 | $this->_clearDir($path . DS); 439 | @rmdir($path); 440 | } else { 441 | @unlink($path); 442 | } 443 | } 444 | } 445 | closedir($handle); 446 | } 447 | } -------------------------------------------------------------------------------- /vendor/composer/InstalledVersions.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer; 14 | 15 | use Composer\Autoload\ClassLoader; 16 | use Composer\Semver\VersionParser; 17 | 18 | /** 19 | * This class is copied in every Composer installed project and available to all 20 | * 21 | * See also https://getcomposer.org/doc/07-runtime.md#installed-versions 22 | * 23 | * To require its presence, you can require `composer-runtime-api ^2.0` 24 | * 25 | * @final 26 | */ 27 | class InstalledVersions 28 | { 29 | /** 30 | * @var mixed[]|null 31 | * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null 32 | */ 33 | private static $installed; 34 | 35 | /** 36 | * @var bool|null 37 | */ 38 | private static $canGetVendors; 39 | 40 | /** 41 | * @var array[] 42 | * @psalm-var array}> 43 | */ 44 | private static $installedByVendor = array(); 45 | 46 | /** 47 | * Returns a list of all package names which are present, either by being installed, replaced or provided 48 | * 49 | * @return string[] 50 | * @psalm-return list 51 | */ 52 | public static function getInstalledPackages() 53 | { 54 | $packages = array(); 55 | foreach (self::getInstalled() as $installed) { 56 | $packages[] = array_keys($installed['versions']); 57 | } 58 | 59 | if (1 === \count($packages)) { 60 | return $packages[0]; 61 | } 62 | 63 | return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); 64 | } 65 | 66 | /** 67 | * Returns a list of all package names with a specific type e.g. 'library' 68 | * 69 | * @param string $type 70 | * @return string[] 71 | * @psalm-return list 72 | */ 73 | public static function getInstalledPackagesByType($type) 74 | { 75 | $packagesByType = array(); 76 | 77 | foreach (self::getInstalled() as $installed) { 78 | foreach ($installed['versions'] as $name => $package) { 79 | if (isset($package['type']) && $package['type'] === $type) { 80 | $packagesByType[] = $name; 81 | } 82 | } 83 | } 84 | 85 | return $packagesByType; 86 | } 87 | 88 | /** 89 | * Checks whether the given package is installed 90 | * 91 | * This also returns true if the package name is provided or replaced by another package 92 | * 93 | * @param string $packageName 94 | * @param bool $includeDevRequirements 95 | * @return bool 96 | */ 97 | public static function isInstalled($packageName, $includeDevRequirements = true) 98 | { 99 | foreach (self::getInstalled() as $installed) { 100 | if (isset($installed['versions'][$packageName])) { 101 | return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); 102 | } 103 | } 104 | 105 | return false; 106 | } 107 | 108 | /** 109 | * Checks whether the given package satisfies a version constraint 110 | * 111 | * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: 112 | * 113 | * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') 114 | * 115 | * @param VersionParser $parser Install composer/semver to have access to this class and functionality 116 | * @param string $packageName 117 | * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package 118 | * @return bool 119 | */ 120 | public static function satisfies(VersionParser $parser, $packageName, $constraint) 121 | { 122 | $constraint = $parser->parseConstraints($constraint); 123 | $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); 124 | 125 | return $provided->matches($constraint); 126 | } 127 | 128 | /** 129 | * Returns a version constraint representing all the range(s) which are installed for a given package 130 | * 131 | * It is easier to use this via isInstalled() with the $constraint argument if you need to check 132 | * whether a given version of a package is installed, and not just whether it exists 133 | * 134 | * @param string $packageName 135 | * @return string Version constraint usable with composer/semver 136 | */ 137 | public static function getVersionRanges($packageName) 138 | { 139 | foreach (self::getInstalled() as $installed) { 140 | if (!isset($installed['versions'][$packageName])) { 141 | continue; 142 | } 143 | 144 | $ranges = array(); 145 | if (isset($installed['versions'][$packageName]['pretty_version'])) { 146 | $ranges[] = $installed['versions'][$packageName]['pretty_version']; 147 | } 148 | if (array_key_exists('aliases', $installed['versions'][$packageName])) { 149 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); 150 | } 151 | if (array_key_exists('replaced', $installed['versions'][$packageName])) { 152 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); 153 | } 154 | if (array_key_exists('provided', $installed['versions'][$packageName])) { 155 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); 156 | } 157 | 158 | return implode(' || ', $ranges); 159 | } 160 | 161 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 162 | } 163 | 164 | /** 165 | * @param string $packageName 166 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 167 | */ 168 | public static function getVersion($packageName) 169 | { 170 | foreach (self::getInstalled() as $installed) { 171 | if (!isset($installed['versions'][$packageName])) { 172 | continue; 173 | } 174 | 175 | if (!isset($installed['versions'][$packageName]['version'])) { 176 | return null; 177 | } 178 | 179 | return $installed['versions'][$packageName]['version']; 180 | } 181 | 182 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 183 | } 184 | 185 | /** 186 | * @param string $packageName 187 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 188 | */ 189 | public static function getPrettyVersion($packageName) 190 | { 191 | foreach (self::getInstalled() as $installed) { 192 | if (!isset($installed['versions'][$packageName])) { 193 | continue; 194 | } 195 | 196 | if (!isset($installed['versions'][$packageName]['pretty_version'])) { 197 | return null; 198 | } 199 | 200 | return $installed['versions'][$packageName]['pretty_version']; 201 | } 202 | 203 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 204 | } 205 | 206 | /** 207 | * @param string $packageName 208 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference 209 | */ 210 | public static function getReference($packageName) 211 | { 212 | foreach (self::getInstalled() as $installed) { 213 | if (!isset($installed['versions'][$packageName])) { 214 | continue; 215 | } 216 | 217 | if (!isset($installed['versions'][$packageName]['reference'])) { 218 | return null; 219 | } 220 | 221 | return $installed['versions'][$packageName]['reference']; 222 | } 223 | 224 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 225 | } 226 | 227 | /** 228 | * @param string $packageName 229 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. 230 | */ 231 | public static function getInstallPath($packageName) 232 | { 233 | foreach (self::getInstalled() as $installed) { 234 | if (!isset($installed['versions'][$packageName])) { 235 | continue; 236 | } 237 | 238 | return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; 239 | } 240 | 241 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 242 | } 243 | 244 | /** 245 | * @return array 246 | * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} 247 | */ 248 | public static function getRootPackage() 249 | { 250 | $installed = self::getInstalled(); 251 | 252 | return $installed[0]['root']; 253 | } 254 | 255 | /** 256 | * Returns the raw installed.php data for custom implementations 257 | * 258 | * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. 259 | * @return array[] 260 | * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} 261 | */ 262 | public static function getRawData() 263 | { 264 | @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); 265 | 266 | if (null === self::$installed) { 267 | // only require the installed.php file if this file is loaded from its dumped location, 268 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 269 | if (substr(__DIR__, -8, 1) !== 'C') { 270 | self::$installed = include __DIR__ . '/installed.php'; 271 | } else { 272 | self::$installed = array(); 273 | } 274 | } 275 | 276 | return self::$installed; 277 | } 278 | 279 | /** 280 | * Returns the raw data of all installed.php which are currently loaded for custom implementations 281 | * 282 | * @return array[] 283 | * @psalm-return list}> 284 | */ 285 | public static function getAllRawData() 286 | { 287 | return self::getInstalled(); 288 | } 289 | 290 | /** 291 | * Lets you reload the static array from another file 292 | * 293 | * This is only useful for complex integrations in which a project needs to use 294 | * this class but then also needs to execute another project's autoloader in process, 295 | * and wants to ensure both projects have access to their version of installed.php. 296 | * 297 | * A typical case would be PHPUnit, where it would need to make sure it reads all 298 | * the data it needs from this class, then call reload() with 299 | * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure 300 | * the project in which it runs can then also use this class safely, without 301 | * interference between PHPUnit's dependencies and the project's dependencies. 302 | * 303 | * @param array[] $data A vendor/composer/installed.php data set 304 | * @return void 305 | * 306 | * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data 307 | */ 308 | public static function reload($data) 309 | { 310 | self::$installed = $data; 311 | self::$installedByVendor = array(); 312 | } 313 | 314 | /** 315 | * @return array[] 316 | * @psalm-return list}> 317 | */ 318 | private static function getInstalled() 319 | { 320 | if (null === self::$canGetVendors) { 321 | self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); 322 | } 323 | 324 | $installed = array(); 325 | 326 | if (self::$canGetVendors) { 327 | foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 328 | if (isset(self::$installedByVendor[$vendorDir])) { 329 | $installed[] = self::$installedByVendor[$vendorDir]; 330 | } elseif (is_file($vendorDir.'/composer/installed.php')) { 331 | $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; 332 | if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { 333 | self::$installed = $installed[count($installed) - 1]; 334 | } 335 | } 336 | } 337 | } 338 | 339 | if (null === self::$installed) { 340 | // only require the installed.php file if this file is loaded from its dumped location, 341 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 342 | if (substr(__DIR__, -8, 1) !== 'C') { 343 | self::$installed = require __DIR__ . '/installed.php'; 344 | } else { 345 | self::$installed = array(); 346 | } 347 | } 348 | $installed[] = self::$installed; 349 | 350 | return $installed; 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /vendor/composer/ClassLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see https://www.php-fig.org/psr/psr-0/ 41 | * @see https://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | /** @var ?string */ 46 | private $vendorDir; 47 | 48 | // PSR-4 49 | /** 50 | * @var array[] 51 | * @psalm-var array> 52 | */ 53 | private $prefixLengthsPsr4 = array(); 54 | /** 55 | * @var array[] 56 | * @psalm-var array> 57 | */ 58 | private $prefixDirsPsr4 = array(); 59 | /** 60 | * @var array[] 61 | * @psalm-var array 62 | */ 63 | private $fallbackDirsPsr4 = array(); 64 | 65 | // PSR-0 66 | /** 67 | * @var array[] 68 | * @psalm-var array> 69 | */ 70 | private $prefixesPsr0 = array(); 71 | /** 72 | * @var array[] 73 | * @psalm-var array 74 | */ 75 | private $fallbackDirsPsr0 = array(); 76 | 77 | /** @var bool */ 78 | private $useIncludePath = false; 79 | 80 | /** 81 | * @var string[] 82 | * @psalm-var array 83 | */ 84 | private $classMap = array(); 85 | 86 | /** @var bool */ 87 | private $classMapAuthoritative = false; 88 | 89 | /** 90 | * @var bool[] 91 | * @psalm-var array 92 | */ 93 | private $missingClasses = array(); 94 | 95 | /** @var ?string */ 96 | private $apcuPrefix; 97 | 98 | /** 99 | * @var self[] 100 | */ 101 | private static $registeredLoaders = array(); 102 | 103 | /** 104 | * @param ?string $vendorDir 105 | */ 106 | public function __construct($vendorDir = null) 107 | { 108 | $this->vendorDir = $vendorDir; 109 | } 110 | 111 | /** 112 | * @return string[] 113 | */ 114 | public function getPrefixes() 115 | { 116 | if (!empty($this->prefixesPsr0)) { 117 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 118 | } 119 | 120 | return array(); 121 | } 122 | 123 | /** 124 | * @return array[] 125 | * @psalm-return array> 126 | */ 127 | public function getPrefixesPsr4() 128 | { 129 | return $this->prefixDirsPsr4; 130 | } 131 | 132 | /** 133 | * @return array[] 134 | * @psalm-return array 135 | */ 136 | public function getFallbackDirs() 137 | { 138 | return $this->fallbackDirsPsr0; 139 | } 140 | 141 | /** 142 | * @return array[] 143 | * @psalm-return array 144 | */ 145 | public function getFallbackDirsPsr4() 146 | { 147 | return $this->fallbackDirsPsr4; 148 | } 149 | 150 | /** 151 | * @return string[] Array of classname => path 152 | * @psalm-return array 153 | */ 154 | public function getClassMap() 155 | { 156 | return $this->classMap; 157 | } 158 | 159 | /** 160 | * @param string[] $classMap Class to filename map 161 | * @psalm-param array $classMap 162 | * 163 | * @return void 164 | */ 165 | public function addClassMap(array $classMap) 166 | { 167 | if ($this->classMap) { 168 | $this->classMap = array_merge($this->classMap, $classMap); 169 | } else { 170 | $this->classMap = $classMap; 171 | } 172 | } 173 | 174 | /** 175 | * Registers a set of PSR-0 directories for a given prefix, either 176 | * appending or prepending to the ones previously set for this prefix. 177 | * 178 | * @param string $prefix The prefix 179 | * @param string[]|string $paths The PSR-0 root directories 180 | * @param bool $prepend Whether to prepend the directories 181 | * 182 | * @return void 183 | */ 184 | public function add($prefix, $paths, $prepend = false) 185 | { 186 | if (!$prefix) { 187 | if ($prepend) { 188 | $this->fallbackDirsPsr0 = array_merge( 189 | (array) $paths, 190 | $this->fallbackDirsPsr0 191 | ); 192 | } else { 193 | $this->fallbackDirsPsr0 = array_merge( 194 | $this->fallbackDirsPsr0, 195 | (array) $paths 196 | ); 197 | } 198 | 199 | return; 200 | } 201 | 202 | $first = $prefix[0]; 203 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 204 | $this->prefixesPsr0[$first][$prefix] = (array) $paths; 205 | 206 | return; 207 | } 208 | if ($prepend) { 209 | $this->prefixesPsr0[$first][$prefix] = array_merge( 210 | (array) $paths, 211 | $this->prefixesPsr0[$first][$prefix] 212 | ); 213 | } else { 214 | $this->prefixesPsr0[$first][$prefix] = array_merge( 215 | $this->prefixesPsr0[$first][$prefix], 216 | (array) $paths 217 | ); 218 | } 219 | } 220 | 221 | /** 222 | * Registers a set of PSR-4 directories for a given namespace, either 223 | * appending or prepending to the ones previously set for this namespace. 224 | * 225 | * @param string $prefix The prefix/namespace, with trailing '\\' 226 | * @param string[]|string $paths The PSR-4 base directories 227 | * @param bool $prepend Whether to prepend the directories 228 | * 229 | * @throws \InvalidArgumentException 230 | * 231 | * @return void 232 | */ 233 | public function addPsr4($prefix, $paths, $prepend = false) 234 | { 235 | if (!$prefix) { 236 | // Register directories for the root namespace. 237 | if ($prepend) { 238 | $this->fallbackDirsPsr4 = array_merge( 239 | (array) $paths, 240 | $this->fallbackDirsPsr4 241 | ); 242 | } else { 243 | $this->fallbackDirsPsr4 = array_merge( 244 | $this->fallbackDirsPsr4, 245 | (array) $paths 246 | ); 247 | } 248 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 249 | // Register directories for a new namespace. 250 | $length = strlen($prefix); 251 | if ('\\' !== $prefix[$length - 1]) { 252 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 253 | } 254 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 255 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 256 | } elseif ($prepend) { 257 | // Prepend directories for an already registered namespace. 258 | $this->prefixDirsPsr4[$prefix] = array_merge( 259 | (array) $paths, 260 | $this->prefixDirsPsr4[$prefix] 261 | ); 262 | } else { 263 | // Append directories for an already registered namespace. 264 | $this->prefixDirsPsr4[$prefix] = array_merge( 265 | $this->prefixDirsPsr4[$prefix], 266 | (array) $paths 267 | ); 268 | } 269 | } 270 | 271 | /** 272 | * Registers a set of PSR-0 directories for a given prefix, 273 | * replacing any others previously set for this prefix. 274 | * 275 | * @param string $prefix The prefix 276 | * @param string[]|string $paths The PSR-0 base directories 277 | * 278 | * @return void 279 | */ 280 | public function set($prefix, $paths) 281 | { 282 | if (!$prefix) { 283 | $this->fallbackDirsPsr0 = (array) $paths; 284 | } else { 285 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 286 | } 287 | } 288 | 289 | /** 290 | * Registers a set of PSR-4 directories for a given namespace, 291 | * replacing any others previously set for this namespace. 292 | * 293 | * @param string $prefix The prefix/namespace, with trailing '\\' 294 | * @param string[]|string $paths The PSR-4 base directories 295 | * 296 | * @throws \InvalidArgumentException 297 | * 298 | * @return void 299 | */ 300 | public function setPsr4($prefix, $paths) 301 | { 302 | if (!$prefix) { 303 | $this->fallbackDirsPsr4 = (array) $paths; 304 | } else { 305 | $length = strlen($prefix); 306 | if ('\\' !== $prefix[$length - 1]) { 307 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 308 | } 309 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 310 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 311 | } 312 | } 313 | 314 | /** 315 | * Turns on searching the include path for class files. 316 | * 317 | * @param bool $useIncludePath 318 | * 319 | * @return void 320 | */ 321 | public function setUseIncludePath($useIncludePath) 322 | { 323 | $this->useIncludePath = $useIncludePath; 324 | } 325 | 326 | /** 327 | * Can be used to check if the autoloader uses the include path to check 328 | * for classes. 329 | * 330 | * @return bool 331 | */ 332 | public function getUseIncludePath() 333 | { 334 | return $this->useIncludePath; 335 | } 336 | 337 | /** 338 | * Turns off searching the prefix and fallback directories for classes 339 | * that have not been registered with the class map. 340 | * 341 | * @param bool $classMapAuthoritative 342 | * 343 | * @return void 344 | */ 345 | public function setClassMapAuthoritative($classMapAuthoritative) 346 | { 347 | $this->classMapAuthoritative = $classMapAuthoritative; 348 | } 349 | 350 | /** 351 | * Should class lookup fail if not found in the current class map? 352 | * 353 | * @return bool 354 | */ 355 | public function isClassMapAuthoritative() 356 | { 357 | return $this->classMapAuthoritative; 358 | } 359 | 360 | /** 361 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 362 | * 363 | * @param string|null $apcuPrefix 364 | * 365 | * @return void 366 | */ 367 | public function setApcuPrefix($apcuPrefix) 368 | { 369 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 370 | } 371 | 372 | /** 373 | * The APCu prefix in use, or null if APCu caching is not enabled. 374 | * 375 | * @return string|null 376 | */ 377 | public function getApcuPrefix() 378 | { 379 | return $this->apcuPrefix; 380 | } 381 | 382 | /** 383 | * Registers this instance as an autoloader. 384 | * 385 | * @param bool $prepend Whether to prepend the autoloader or not 386 | * 387 | * @return void 388 | */ 389 | public function register($prepend = false) 390 | { 391 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 392 | 393 | if (null === $this->vendorDir) { 394 | return; 395 | } 396 | 397 | if ($prepend) { 398 | self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; 399 | } else { 400 | unset(self::$registeredLoaders[$this->vendorDir]); 401 | self::$registeredLoaders[$this->vendorDir] = $this; 402 | } 403 | } 404 | 405 | /** 406 | * Unregisters this instance as an autoloader. 407 | * 408 | * @return void 409 | */ 410 | public function unregister() 411 | { 412 | spl_autoload_unregister(array($this, 'loadClass')); 413 | 414 | if (null !== $this->vendorDir) { 415 | unset(self::$registeredLoaders[$this->vendorDir]); 416 | } 417 | } 418 | 419 | /** 420 | * Loads the given class or interface. 421 | * 422 | * @param string $class The name of the class 423 | * @return true|null True if loaded, null otherwise 424 | */ 425 | public function loadClass($class) 426 | { 427 | if ($file = $this->findFile($class)) { 428 | includeFile($file); 429 | 430 | return true; 431 | } 432 | 433 | return null; 434 | } 435 | 436 | /** 437 | * Finds the path to the file where the class is defined. 438 | * 439 | * @param string $class The name of the class 440 | * 441 | * @return string|false The path if found, false otherwise 442 | */ 443 | public function findFile($class) 444 | { 445 | // class map lookup 446 | if (isset($this->classMap[$class])) { 447 | return $this->classMap[$class]; 448 | } 449 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 450 | return false; 451 | } 452 | if (null !== $this->apcuPrefix) { 453 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 454 | if ($hit) { 455 | return $file; 456 | } 457 | } 458 | 459 | $file = $this->findFileWithExtension($class, '.php'); 460 | 461 | // Search for Hack files if we are running on HHVM 462 | if (false === $file && defined('HHVM_VERSION')) { 463 | $file = $this->findFileWithExtension($class, '.hh'); 464 | } 465 | 466 | if (null !== $this->apcuPrefix) { 467 | apcu_add($this->apcuPrefix.$class, $file); 468 | } 469 | 470 | if (false === $file) { 471 | // Remember that this class does not exist. 472 | $this->missingClasses[$class] = true; 473 | } 474 | 475 | return $file; 476 | } 477 | 478 | /** 479 | * Returns the currently registered loaders indexed by their corresponding vendor directories. 480 | * 481 | * @return self[] 482 | */ 483 | public static function getRegisteredLoaders() 484 | { 485 | return self::$registeredLoaders; 486 | } 487 | 488 | /** 489 | * @param string $class 490 | * @param string $ext 491 | * @return string|false 492 | */ 493 | private function findFileWithExtension($class, $ext) 494 | { 495 | // PSR-4 lookup 496 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 497 | 498 | $first = $class[0]; 499 | if (isset($this->prefixLengthsPsr4[$first])) { 500 | $subPath = $class; 501 | while (false !== $lastPos = strrpos($subPath, '\\')) { 502 | $subPath = substr($subPath, 0, $lastPos); 503 | $search = $subPath . '\\'; 504 | if (isset($this->prefixDirsPsr4[$search])) { 505 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 506 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 507 | if (file_exists($file = $dir . $pathEnd)) { 508 | return $file; 509 | } 510 | } 511 | } 512 | } 513 | } 514 | 515 | // PSR-4 fallback dirs 516 | foreach ($this->fallbackDirsPsr4 as $dir) { 517 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 518 | return $file; 519 | } 520 | } 521 | 522 | // PSR-0 lookup 523 | if (false !== $pos = strrpos($class, '\\')) { 524 | // namespaced class name 525 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 526 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 527 | } else { 528 | // PEAR-like class name 529 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 530 | } 531 | 532 | if (isset($this->prefixesPsr0[$first])) { 533 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 534 | if (0 === strpos($class, $prefix)) { 535 | foreach ($dirs as $dir) { 536 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 537 | return $file; 538 | } 539 | } 540 | } 541 | } 542 | } 543 | 544 | // PSR-0 fallback dirs 545 | foreach ($this->fallbackDirsPsr0 as $dir) { 546 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 547 | return $file; 548 | } 549 | } 550 | 551 | // PSR-0 include paths. 552 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 553 | return $file; 554 | } 555 | 556 | return false; 557 | } 558 | } 559 | 560 | /** 561 | * Scope isolated include. 562 | * 563 | * Prevents access to $this/self from included files. 564 | * 565 | * @param string $file 566 | * @return void 567 | * @private 568 | */ 569 | function includeFile($file) 570 | { 571 | include $file; 572 | } 573 | -------------------------------------------------------------------------------- /startmvc/core/Router.php: -------------------------------------------------------------------------------- 1 | '(\d+)', 73 | ':slug' => '([a-z0-9-]+)', 74 | ':any' => '(.+)', 75 | ':num' => '([0-9]+)', 76 | ':alpha' => '([a-zA-Z]+)', 77 | ':alphanum' => '([a-zA-Z0-9]+)' 78 | ]; 79 | 80 | /** 81 | * 简单模式替换规则 82 | * @var array 83 | */ 84 | protected static $simplePatterns = [ 85 | '(:any)' => '(.+)', 86 | '(:num)' => '([0-9]+)', 87 | '(:alpha)' => '([a-zA-Z]+)', 88 | '(:alphanum)' => '([a-zA-Z0-9]+)' 89 | ]; 90 | 91 | /** 92 | * 添加GET路由 93 | * @param string $uri 路由URI 94 | * @param mixed $action 控制器方法或回调函数 95 | * @param array $middleware 中间件数组 96 | * @return void 97 | */ 98 | public static function get($uri, $action, $middleware = []) 99 | { 100 | self::addRoute('GET', $uri, $action, $middleware); 101 | } 102 | 103 | /** 104 | * 添加POST路由 105 | * @param string $uri 路由URI 106 | * @param mixed $action 控制器方法或回调函数 107 | * @param array $middleware 中间件数组 108 | * @return void 109 | */ 110 | public static function post($uri, $action, $middleware = []) 111 | { 112 | self::addRoute('POST', $uri, $action, $middleware); 113 | } 114 | 115 | /** 116 | * 添加PUT路由 117 | * @param string $uri 路由URI 118 | * @param mixed $action 控制器方法或回调函数 119 | * @param array $middleware 中间件数组 120 | * @return void 121 | */ 122 | public static function put($uri, $action, $middleware = []) 123 | { 124 | self::addRoute('PUT', $uri, $action, $middleware); 125 | } 126 | 127 | /** 128 | * 添加DELETE路由 129 | * @param string $uri 路由URI 130 | * @param mixed $action 控制器方法或回调函数 131 | * @param array $middleware 中间件数组 132 | * @return void 133 | */ 134 | public static function delete($uri, $action, $middleware = []) 135 | { 136 | self::addRoute('DELETE', $uri, $action, $middleware); 137 | } 138 | 139 | /** 140 | * 添加支持任意HTTP方法的路由 141 | * @param string $uri 路由URI 142 | * @param mixed $action 控制器方法或回调函数 143 | * @param array $middleware 中间件数组 144 | * @return void 145 | */ 146 | public static function any($uri, $action, $middleware = []) 147 | { 148 | $methods = ['GET', 'POST', 'PUT', 'DELETE']; 149 | foreach ($methods as $method) { 150 | self::addRoute($method, $uri, $action, $middleware); 151 | } 152 | } 153 | 154 | /** 155 | * 创建路由组 156 | * @param array|string $attributes 路由组属性或前缀 157 | * @param callable $callback 路由定义回调 158 | * @return void 159 | */ 160 | public static function group($attributes, callable $callback) 161 | { 162 | // 保存当前组状态 163 | $previousPrefix = self::$prefix; 164 | $previousMiddleware = self::$middleware; 165 | 166 | // 设置新组属性 167 | if (is_string($attributes)) { 168 | self::$prefix .= $attributes; 169 | } else { 170 | if (isset($attributes['prefix'])) { 171 | self::$prefix .= '/' . trim($attributes['prefix'], '/'); 172 | } 173 | 174 | if (isset($attributes['middleware'])) { 175 | $middleware = (array) $attributes['middleware']; 176 | self::$middleware = array_merge(self::$middleware, $middleware); 177 | } 178 | } 179 | 180 | // 执行回调 181 | $callback(); 182 | 183 | // 恢复先前状态 184 | self::$prefix = $previousPrefix; 185 | self::$middleware = $previousMiddleware; 186 | } 187 | 188 | /** 189 | * 添加RESTful资源路由 190 | * @param string $name 资源名称 191 | * @param string $controller 控制器类 192 | * @return void 193 | */ 194 | public static function resource($name, $controller) 195 | { 196 | $name = trim($name, '/'); 197 | self::get("/$name", "$controller@index"); 198 | self::get("/$name/create", "$controller@create"); 199 | self::post("/$name", "$controller@store"); 200 | self::get("/$name/:id", "$controller@show"); 201 | self::get("/$name/:id/edit", "$controller@edit"); 202 | self::put("/$name/:id", "$controller@update"); 203 | self::delete("/$name/:id", "$controller@destroy"); 204 | } 205 | 206 | /** 207 | * 添加路由规则 208 | * @param string $method HTTP方法 209 | * @param string $uri 路由URI 210 | * @param mixed $action 控制器方法或回调函数 211 | * @param array $middleware 中间件数组 212 | * @return void 213 | */ 214 | protected static function addRoute($method, $uri, $action, $middleware = []) 215 | { 216 | // 处理前缀 217 | $uri = self::$prefix . '/' . trim($uri, '/'); 218 | $uri = trim($uri, '/'); 219 | if (empty($uri)) { 220 | $uri = '/'; 221 | } 222 | 223 | // 存储路由 224 | self::$routes[$method][$uri] = [ 225 | 'action' => $action, 226 | 'middleware' => $middleware 227 | ]; 228 | } 229 | 230 | /** 231 | * 根据URI和方法匹配路由 232 | * @param string $uri 请求URI 233 | * @param string $method HTTP方法 234 | * @return array|null 匹配的路由和参数 235 | */ 236 | public static function match($uri, $method) 237 | { 238 | $uri = trim($uri, '/'); 239 | if (empty($uri)) { 240 | $uri = '/'; 241 | } 242 | 243 | // 检查精确匹配 244 | if (isset(self::$routes[$method][$uri])) { 245 | return [self::$routes[$method][$uri], []]; 246 | } 247 | 248 | // 检查模式匹配 249 | foreach (self::$routes[$method] ?? [] as $route => $data) { 250 | $pattern = self::compileRoute($route); 251 | if (preg_match('#^' . $pattern . '$#', $uri, $matches)) { 252 | array_shift($matches); // 移除完整匹配 253 | return [$data, $matches]; 254 | } 255 | } 256 | 257 | return null; 258 | } 259 | 260 | /** 261 | * 将路由转换为正则表达式 262 | * @param string $route 路由URI 263 | * @return string 编译后的正则表达式 264 | */ 265 | protected static function compileRoute($route) 266 | { 267 | if (strpos($route, ':') !== false) { 268 | foreach (self::$patterns as $key => $pattern) { 269 | $route = str_replace($key, $pattern, $route); 270 | } 271 | } 272 | 273 | return str_replace('/', '\/', $route); 274 | } 275 | 276 | /** 277 | * 从配置文件加载路由定义 278 | * @param array $routes 路由配置数组 279 | * @return void 280 | */ 281 | public static function loadFromConfig(array $routes) 282 | { 283 | foreach ($routes as $route) { 284 | if (is_array($route) && count($route) >= 2) { 285 | $pattern = $route[0]; 286 | $action = $route[1]; 287 | 288 | // 检查是否为正则表达式格式 /pattern/ 289 | if (is_string($pattern) && strlen($pattern) > 2 && $pattern[0] === '/' && $pattern[strlen($pattern) - 1] === '/') { 290 | // 正则表达式路由 291 | self::regexRoute('GET', $pattern, $action); 292 | } else { 293 | // 简单模式路由 294 | self::simpleRoute('GET', $pattern, $action); 295 | } 296 | } 297 | } 298 | } 299 | 300 | /** 301 | * 添加简单模式路由 302 | * @param string $method HTTP方法 303 | * @param string $pattern 路由模式 304 | * @param string $action 控制器路径 305 | * @return void 306 | */ 307 | public static function simpleRoute($method, $pattern, $action) 308 | { 309 | // 转换简单模式为正则表达式 310 | $regex = $pattern; 311 | foreach (self::$simplePatterns as $key => $replacement) { 312 | $regex = str_replace($key, $replacement, $regex); 313 | } 314 | 315 | // 如果不是正则表达式,将其转换为精确匹配的正则 316 | if ($regex === $pattern) { 317 | $regex = '/^' . preg_quote($pattern, '/') . '$/'; 318 | } else { 319 | $regex = '/^' . str_replace('/', '\/', $regex) . '$/'; 320 | } 321 | 322 | self::$routes['config'][] = [ 323 | 'type' => 'simple', 324 | 'method' => $method, 325 | 'pattern' => $pattern, 326 | 'regex' => $regex, 327 | 'action' => $action 328 | ]; 329 | } 330 | 331 | /** 332 | * 添加正则表达式路由 333 | * @param string $method HTTP方法 334 | * @param string $regex 正则表达式 335 | * @param string $action 控制器路径 336 | * @return void 337 | */ 338 | public static function regexRoute($method, $regex, $action) 339 | { 340 | self::$routes['config'][] = [ 341 | 'type' => 'regex', 342 | 'method' => $method, 343 | 'regex' => $regex, 344 | 'action' => $action 345 | ]; 346 | } 347 | 348 | /** 349 | * 解析路由并执行匹配的操作 350 | * @return mixed 351 | * @throws \Exception 未找到路由时抛出异常 352 | */ 353 | public static function dispatch() 354 | { 355 | $uri = $_SERVER['REQUEST_URI']; 356 | 357 | // 移除查询字符串 358 | if (strpos($uri, '?') !== false) { 359 | $uri = substr($uri, 0, strpos($uri, '?')); 360 | } 361 | 362 | $method = $_SERVER['REQUEST_METHOD']; 363 | 364 | // 处理PUT、DELETE请求 365 | if ($method === 'POST' && isset($_POST['_method'])) { 366 | $method = strtoupper($_POST['_method']); 367 | } 368 | 369 | // 先尝试匹配新的路由格式 370 | $result = self::match($uri, $method); 371 | if ($result !== null) { 372 | list($route, $params) = $result; 373 | 374 | // 获取路由中间件 375 | $routeMiddleware = $route['middleware'] ?? []; 376 | 377 | // 创建请求对象 378 | $request = new Request(); 379 | 380 | // 应用路由特定的中间件 381 | $response = Middleware::pipeline($routeMiddleware, $request, function($request) use ($route, $params) { 382 | // 执行控制器方法或回调 383 | $action = $route['action']; 384 | 385 | if (is_callable($action)) { 386 | return call_user_func_array($action, $params); 387 | } 388 | 389 | // 解析控制器和方法 390 | if (is_string($action)) { 391 | list($controller, $method) = explode('@', $action); 392 | $controller = 'app\\controllers\\' . $controller; 393 | $instance = new $controller(); 394 | return call_user_func_array([$instance, $method], $params); 395 | } 396 | }); 397 | 398 | return $response; 399 | } 400 | 401 | // 尝试匹配配置文件中的路由 402 | $configResult = self::matchConfigRoutes($uri); 403 | if ($configResult !== null) { 404 | list($target, $params) = $configResult; 405 | 406 | // 处理控制器路径 407 | $parts = explode('/', $target); 408 | if (count($parts) >= 2) { 409 | $methodName = self::convertAction(array_pop($parts)); 410 | $controllerName = self::convertController(array_pop($parts)); 411 | $namespace = !empty($parts) ? implode('\\', $parts) : 'app\\controllers'; 412 | 413 | $controllerClass = $namespace . '\\' . $controllerName . 'Controller'; 414 | $controller = new $controllerClass(); 415 | return call_user_func_array([$controller, $methodName], $params); 416 | } else { 417 | throw new \Exception("Invalid route target: $target", 500); 418 | } 419 | } 420 | 421 | // 如果没有匹配到路由,尝试使用默认解析方式 422 | $parseResult = self::parse($uri); 423 | if ($parseResult && count($parseResult) >= 3) { 424 | list($module, $controller, $action, $params) = $parseResult; 425 | 426 | // 构建控制器类名 427 | $controllerClass = 'app\\' . $module . '\\controller\\' . ucfirst($controller) . 'Controller'; 428 | 429 | // 检查控制器类是否存在 430 | if (class_exists($controllerClass)) { 431 | $controllerInstance = new $controllerClass(); 432 | 433 | // 检查方法是否存在 434 | if (method_exists($controllerInstance, $action)) { 435 | return call_user_func_array([$controllerInstance, $action], $params ?: []); 436 | } 437 | } 438 | } 439 | 440 | throw new \Exception("Route not found: $uri [$method]", 404); 441 | } 442 | 443 | /** 444 | * 匹配配置文件中定义的路由 445 | * @param string $uri 请求URI 446 | * @return array|null 匹配的目标和参数 447 | */ 448 | protected static function matchConfigRoutes($uri) 449 | { 450 | $uri = trim($uri, '/'); 451 | 452 | foreach (self::$routes['config'] ?? [] as $route) { 453 | $pattern = $route['regex']; 454 | $target = $route['action']; 455 | 456 | // 移除分隔符 457 | if ($route['type'] === 'regex') { 458 | $pattern = substr($pattern, 1, -1); 459 | } 460 | 461 | if (preg_match($pattern, $uri, $matches)) { 462 | array_shift($matches); // 移除完整匹配 463 | 464 | // 替换目标中的 $1, $2 等为实际参数 465 | $replacedTarget = preg_replace_callback('/\$(\d+)/', function($m) use ($matches) { 466 | $index = intval($m[1]) - 1; 467 | return isset($matches[$index]) ? $matches[$index] : ''; 468 | }, $target); 469 | 470 | return [$replacedTarget, $matches]; 471 | } 472 | } 473 | 474 | return null; 475 | } 476 | 477 | /** 478 | * 转换URL片段为控制器名称 (StudlyCase) 479 | * @param string $part 480 | * @return string 481 | */ 482 | protected static function convertController($part) 483 | { 484 | return str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', strtolower($part)))); 485 | } 486 | 487 | /** 488 | * 转换URL片段为方法名称 (camelCase) 489 | * @param string $part 490 | * @return string 491 | */ 492 | protected static function convertAction($part) 493 | { 494 | $studly = str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', strtolower($part)))); 495 | return lcfirst($studly); 496 | } 497 | 498 | /** 499 | * 解析路由规则 500 | * @param string $uri 请求URI 501 | * @return array 解析结果 502 | */ 503 | public static function parse($uri) 504 | { 505 | // 移除查询字符串 506 | if (strpos($uri, '?') !== false) { 507 | $uri = substr($uri, 0, strpos($uri, '?')); 508 | } 509 | 510 | // 移除前后的斜杠 511 | $uri = trim($uri, '/'); 512 | 513 | // 获取默认模块配置 514 | $defaultModule = config('default_module') ?: 'home'; 515 | $defaultController = config('default_controller') ?: 'Index'; 516 | $defaultAction = config('default_action') ?: 'index'; 517 | 518 | // 如果URI为空,设置为首页 519 | if (empty($uri)) { 520 | return [$defaultModule, $defaultController, $defaultAction, []]; 521 | } 522 | 523 | // 智能处理URL后缀 524 | $originalUri = $uri; 525 | $urlSuffix = config('common.url_suffix') ?: ''; 526 | 527 | // 如果配置了URL后缀且URI以该后缀结尾,则移除后缀进行路由匹配 528 | if (!empty($urlSuffix) && strlen($uri) > strlen($urlSuffix)) { 529 | $suffixPos = strrpos($uri, $urlSuffix); 530 | if ($suffixPos !== false && $suffixPos == strlen($uri) - strlen($urlSuffix)) { 531 | $uri = substr($uri, 0, $suffixPos); 532 | } 533 | } 534 | 535 | // 加载路由配置 536 | $routes = \startmvc\core\Config::load('route') ?: []; 537 | 538 | // 遍历配置的路由规则 539 | foreach ($routes as $route) { 540 | if (is_array($route) && count($route) >= 2) { 541 | $pattern = $route[0]; 542 | $target = $route[1]; 543 | 544 | // 处理正则表达式路由 545 | if (is_string($pattern) && strlen($pattern) > 2 && $pattern[0] === '/' && $pattern[strlen($pattern) - 1] === '/') { 546 | if (preg_match($pattern, $uri, $matches)) { 547 | // 替换目标中的 $1, $2 等为实际参数 548 | $target = preg_replace_callback('/\$(\d+)/', function($m) use ($matches) { 549 | $index = intval($m[1]); 550 | return isset($matches[$index]) ? $matches[$index] : ''; 551 | }, $target); 552 | 553 | $parts = explode('/', $target); 554 | return [ 555 | isset($parts[0]) ? strtolower($parts[0]) : $defaultModule, 556 | isset($parts[1]) ? self::convertController($parts[1]) : $defaultController, 557 | isset($parts[2]) ? self::convertAction($parts[2]) : $defaultAction, 558 | array_slice($parts, 3) 559 | ]; 560 | } 561 | } 562 | // 处理简单模式路由 563 | else { 564 | $regex = $pattern; 565 | foreach (self::$simplePatterns as $key => $replacement) { 566 | $regex = str_replace($key, $replacement, $regex); 567 | } 568 | 569 | if (preg_match('#^' . $regex . '$#', $uri, $matches)) { 570 | array_shift($matches); // 移除完整匹配 571 | $parts = explode('/', $target); 572 | 573 | // 替换目标中的参数 574 | foreach ($matches as $i => $match) { 575 | $target = str_replace('$' . ($i + 1), $match, $target); 576 | } 577 | 578 | $parts = explode('/', $target); 579 | return [ 580 | isset($parts[0]) ? strtolower($parts[0]) : $defaultModule, 581 | isset($parts[1]) ? self::convertController($parts[1]) : $defaultController, 582 | isset($parts[2]) ? self::convertAction($parts[2]) : $defaultAction, 583 | array_slice($parts, 3) 584 | ]; 585 | } 586 | } 587 | } 588 | } 589 | 590 | // 如果没有匹配的路由规则,使用默认的解析方式 591 | $parts = explode('/', $uri); 592 | 593 | // 智能解析:尝试判断是否省略了默认模块 594 | if (count($parts) >= 1) { 595 | // 检查第一个部分是否是已存在的模块 596 | $possibleModule = strtolower($parts[0]); // 模块名转小写进行检查 597 | 598 | // 使用绝对路径检查模块目录 599 | if (defined('APP_PATH')) { 600 | $modulePath = APP_PATH . $possibleModule; 601 | } else { 602 | // 如果 APP_PATH 未定义,使用相对路径 603 | $modulePath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . $possibleModule; 604 | } 605 | 606 | 607 | 608 | // 如果模块目录存在,按正常方式解析 609 | if (is_dir($modulePath)) { 610 | return [ 611 | $possibleModule, // 使用小写的模块名 612 | isset($parts[1]) ? self::convertController($parts[1]) : $defaultController, // 控制器名StudlyCase 613 | isset($parts[2]) ? self::convertAction($parts[2]) : $defaultAction, // 方法名camelCase 614 | array_slice($parts, 3) 615 | ]; 616 | } else { 617 | // 如果模块目录不存在,假设省略了默认模块,将第一个部分作为控制器 618 | return [ 619 | $defaultModule, 620 | isset($parts[0]) ? self::convertController($parts[0]) : $defaultController, // 控制器名StudlyCase 621 | isset($parts[1]) ? self::convertAction($parts[1]) : $defaultAction, // 方法名camelCase 622 | array_slice($parts, 2) 623 | ]; 624 | } 625 | } 626 | 627 | return [ 628 | $defaultModule, 629 | $defaultController, 630 | $defaultAction, 631 | [] 632 | ]; 633 | } 634 | } --------------------------------------------------------------------------------