\bff\middleware\TrustedProxies::class, 'priority' => 5], ['callback' => \bff\middleware\FrameGuard::class, 'priority' => 10], ['callback' => \bff\middleware\Cors::class, 'priority' => 15], ]; /** @var array список middleware групп */ protected $middlewareGroups = [ 'web' => [ 'session' => ['callback' => \bff\middleware\StartSession::class, 'priority' => 50], ], ]; /** @var string|null текущий контекст приложения */ protected $context; /** @var bool режим разработчика */ protected $developersMode = false; /** @var array|Logger[] inited loggers */ protected $loggers = []; /** @var array */ protected $components = [ 'admin' => '\bff\admin\Admin', 'admin.menu' => '\AdminMenu', 'auth' => '\bff\auth\AuthManager', 'captcha' => '\bff\captcha\CaptchaManager', 'currency' => '\bff\currency\Currency', 'config' => '\bff\base\Config', 'cron' => '\bff\base\CronManager', 'events' => '\bff\base\Events', 'errors' => '\bff\base\Errors', 'files' => '\Illuminate\Filesystem\Filesystem', 'hash' => '\bff\base\HashManager', 'hooks' => '\bff\extend\Hooks', 'locale' => '\bff\base\Locale', 'providers' => '\bff\base\Providers', 'redirect' => '\bff\http\RedirectFactory', 'response' => '\bff\http\Response', 'router' => '\bff\base\Router', 'security' => '\app\Security', 'session' => '\bff\base\SessionManager', 'tags' => '\bff\extend\Tags', 'url' => '\bff\base\Url', 'view' => '\bff\base\View', ]; /** @var array */ protected $classExtensions = []; /** * @var array autoload карта * [ * 'alias-класс' => 'оригинальный класс', * 'имя класса' => ['ключ группы: [app, core]', 'путь к файлу, относительно директории группы'] * ] */ protected $autoloadMap = [ # aliases 'bff' => '\bff\facades\bff', 'Admin' => '\bff\facades\Admin', 'Auth' => '\bff\facades\Auth', 'Cache' => '\bff\facades\Cache', 'Captcha' => '\bff\facades\Captcha', 'config' => '\bff\facades\Config', 'Crypt' => '\bff\facades\Crypt', 'Currency' => '\bff\facades\Currency', 'Errors' => '\bff\facades\Errors', 'Event' => '\bff\facades\Event', 'Input' => '\bff\facades\Input', 'File' => '\bff\facades\File', 'Hash' => '\bff\facades\Hash', 'Hooks' => '\bff\facades\Hooks', 'Http' => '\bff\facades\Http', 'Lang' => '\bff\facades\Lang', 'Providers' => '\bff\facades\Providers', 'Request' => '\bff\facades\Request', 'Redirect' => '\bff\facades\Redirect', 'Response' => '\bff\facades\Response', 'Route' => '\bff\facades\Route', 'Security' => '\bff\facades\Security', 'Session' => '\bff\facades\Session', 'Url' => '\bff\facades\Url', 'User' => '\bff\facades\User', 'View' => '\bff\facades\View', 'js' => '\bff\base\js', 'HTML' => '\bff\utils\HTML', 'Component' => '\bff\base\Component', 'func' => '\bff\utils\func', 'Model' => '\bff\db\Model', 'Hook' => '\bff\extend\Hook', 'Plugin' => '\bff\extend\Plugin', 'PluginAddon' => '\bff\extend\plugin\Addon', 'ThemeAddon' => '\bff\extend\theme\Addon', 'Pagination' => '\bff\utils\Pagination', 'Logger' => '\bff\logs\Logger', 'AdminMenu' => '\bff\admin\Menu', 'CMail' => '\bff\external\Mail', # deprecated # autoload 'UsersSocial' => ['app', 'modules/users/users.social.php'], 'Parsedown' => ['core', 'external/parsedown/Parsedown.php'], 'Minifier' => ['core', 'external/Minifier.php'], # app 'tpl' => ['app', 'app/tpl.php'], 'Theme' => ['app', 'app/theme/Base.php'], 'tplAdmin' => ['app', 'app/tplAdmin.php'], 'Module' => ['app', 'app/Module.php'], 'UsersAvatar' => '\modules\users\Avatar', ]; /** @var array */ protected $autoloadNamespaces = []; /** * Get application instance. * @return static */ public static function i() { return static::getInstance(); } /** * Create a new Application instance. * @param string|null $pathBase * @return void */ public function __construct(?string $pathBase = null) { if ($pathBase) { $this->setBasePath($pathBase); } $this->instance('app', static::setInstance($this)); $this->instance(Container::class, $this); $this->init(); } /** * Init application. * @return void */ protected function init() { $this->initAutoload(); $this->initContainerAliases(); $this->initComponents(); $this->initConfig(); $this->initErrors(); $this->input(); # Core modules foreach (['dev'] as $module) { $this->moduleRegister($module, $this->corePath('modules/' . $module), [ 'class' => "bff\\modules\\{$module}\\{$module}", 'routes' => false, # register later \/ ]); } # Theme $this->theme(); # Modules foreach ($this->getModulesList() as $module) { # Hooks $this->autoloadMap[ucfirst($module['name']) . 'Hooks'] = ['app', 'modules/' . $module['name'] . '/hooks.php']; # Routes $this->router()->addMany($this->view()->resolveFileInPath('routes.php', $module['path'])); } # Plugins Dev::pluginsLoad(); # Base application inited hook $this->hook('app.init'); } protected function initAutoload() { # Facades & aliases Facade::clearResolvedInstances(); Facade::setFacadeApplication($this); # Autoload spl_autoload_register([$this, 'autoload']); } /** * Register the core class aliases in the container. */ protected function initContainerAliases() { foreach ( [ 'app' => [static::class, \bff\contracts\Application::class, \bff\contracts\Container::class, \Illuminate\Contracts\Container\Container::class, \Psr\Container\ContainerInterface::class], 'auth' => [\bff\auth\AuthManager::class], 'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class], 'cache' => [\bff\cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class], 'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class, \Psr\SimpleCache\CacheInterface::class], 'config' => [\bff\base\Config::class, \Illuminate\Contracts\Config\Repository::class], 'db' => [\bff\db\illuminate\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class], 'db.connection' => [\bff\db\illuminate\Connection::class, \Illuminate\Database\ConnectionInterface::class], 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], 'events' => [\bff\base\Events::class, \Illuminate\Contracts\Events\Dispatcher::class], 'files' => [\Illuminate\Filesystem\Filesystem::class], 'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], 'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class], 'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class], 'hash' => [\bff\base\HashManager::class], 'hash.driver' => [\Illuminate\Contracts\Hashing\Hasher::class], 'redirect' => [\bff\http\RedirectFactory::class], 'session' => [\bff\base\SessionManager::class], 'session.store' => [\Illuminate\Session\Store::class, \Illuminate\Contracts\Session\Session::class], ] as $key => $aliases ) { foreach ($aliases as $alias) { $this->alias($key, $alias); } } } protected function initComponents() { # config $this->components['config'] = static function ($app) { return new \bff\base\Config($app); # ! }; # cache $this->components['cache'] = static function ($app) { return new \bff\cache\CacheManager($app); }; $this->components['cache.store'] = static function ($app) { return $app['cache']->driver(); }; $this->components['memcached.connector'] = static function () { return new \Illuminate\Cache\MemcachedConnector(); }; # session $this->components['session.store'] = static function ($app) { return $app->make('session')->driver(); }; $this->components[\bff\middleware\StartSession::class] = static function ($app) { return new \bff\middleware\StartSession($app->make('session'), function () use ($app) { return $app->make('cache'); }); }; # hashing $this->components['hash.driver'] = static function ($app) { return $app->make('hash')->driver(); }; # encryption $this->components['encrypter'] = static function ($app) { $config = $app->make('config')->get('crypt'); $key = $config['key'] ?? ''; if (mb_stripos($key, 'base64:') === 0) { $key = base64_decode(mb_substr($key, 7)); } return new \Illuminate\Encryption\Encrypter($key, $config['cipher'] ?? 'AES-256-CBC'); }; # auth $this->components['auth.driver'] = static function ($app) { return $app->make('auth')->guard(); }; $this->bind(Authenticatable::class, static function ($app) { return call_user_func($app->make('auth')->userResolver()); }); $this->bind('user', static function ($app) { return call_user_func($app->make('auth')->userResolver()); }); # base $this->components['request'] = static function ($app) { $request = \bff\http\Request::capture(); $request->setUserResolver(function ($guard = null) use ($app) { return call_user_func($app->make('auth')->userResolver(), $guard); }); return $request; }; $this->rebinding('request', function ($app, $request) { $request->setRouteResolver(static function () { return null; }); $request->setUserResolver(static function ($guard = null) use ($app) { return call_user_func($app->make('auth')->userResolver(), $guard); }); foreach ($this->instances as $key => $instance) { if ($instance instanceof contracts\ResetsAfterRequest) { $instance->onNewRequest($request); } if ($instance instanceof contracts\DestroyAfterRequest) { unset($this->instances[$key]); } } return $request; }); $this->components['input'] = static function ($app) { return $app->make('request')->input(); }; $this->components['database'] = static function ($app) { $db = new \bff\db\Database(); $db->connectionConfig('db'); $db->connect(); return $db; }; $this['database_factory'] = static function ($app) { return new \bff\db\Database(); }; foreach ($this->components as $alias => $component) { $this->singleton($alias, $component); } } protected function initConfig() { mb_internal_encoding('UTF-8'); # Load system config define('DB_PREFIX', $this->config('db.prefix')); $this->instance('debug', $this->config('debug', false)); $this->instance('test', (defined('BFF_TEST') ? !empty(BFF_TEST) : $this->config('test', false))); /** @deprecated use localhost() */ define('BFF_LOCALHOST', $this->instance('localhost', $this->config('localhost', false))); define('SITEHOST', $this->instance('host', $this->config('site.host'))); # Config/sys hooks: $this->hooksAdd($this->config('hooks', [])); # Load site config config::file('db.tables'); config::load(); /** @deprecated use {Url::to()} */ define('SITEURL', $this->request()->scheme() . '://' . SITEHOST); define('SITEURL_STATIC', rtrim($this->config('site.static'), '\\/ ')); } /** * Errors handlers init */ protected function initErrors() { $this->errors(); } /** * Set the base path for the application. * @param string $path base path * @return void */ public function setBasePath(string $path) { $this->basePath = rtrim($path, '\/'); $this->instance('path.base', $this->basePath()); $this->instance('path.core', $this->corePath()); $this->instance('path.config', $this->configPath()); $this->instance('path.cache', $this->cachePath()); $this->instance('path.locale', $this->localePath()); $this->instance('path.public', $this->publicPath()); $this->instance('path.modules', $this->modulesPath()); $this->instance('path.plugins', $this->pluginsPath()); $this->instance('path.themes', $this->themesPath()); if (! defined('PATH_CORE')) { /** @deprecated use corePath() */ define('PATH_CORE', $this['path.core']); } if (! defined('PATH_PUBLIC')) { /** @deprecated use publicPath() */ define('PATH_PUBLIC', $this['path.public']); } if (! defined('PATH_MODULES')) { /** @deprecated use modulesPath() */ define('PATH_MODULES', $this['path.modules']); } if (! defined('PATH_PLUGINS')) { /** @deprecated use pluginsPath() */ define('PATH_PLUGINS', $this['path.plugins']); } if (! defined('PATH_THEMES')) { /** @deprecated use themesPath() */ define('PATH_THEMES', $this['path.themes']); } } /** * Get the base path of the application. * @param string $path path to append * @return string */ public function basePath(string $path = ''): string { return str_replace('/', static::DS, $this->basePath . '/' . ltrim($path, '\/')); } /** * Get the path to the application core files. * @param string $path path to append * @return string */ public function corePath(string $path = ''): string { return $this->basePath('bff/' . ltrim($path, '\/')); } /** * Get the path to the application config files. * @param string $path path to append * @return string */ public function configPath(string $path = ''): string { return $this->basePath('config/' . ltrim($path, '\/')); } /** * Get the path to the application cache files. * @param string $path path to append * @return string */ public function cachePath(string $path = ''): string { return $this->basePath('files/cache/' . ltrim($path, '\/')); } /** * Get the path to the application locale files. * @param string $path path to append * @return string */ public function localePath(string $path = ''): string { return $this->basePath('files/locale/' . ltrim($path, '\/')); } /** * Get the path to the application public directory. * @param string $path path to append * @return string */ public function publicPath(string $path = ''): string { return $this->basePath('public_html/' . ltrim($path, '\/')); } /** * Get the path to the application modules directory. * @param string $path path to append * @return string */ public function modulesPath(string $path = ''): string { return $this->basePath('modules/' . ltrim($path, '\/')); } /** * Get the path to the application plugins directory. * @param string $path path to append * @return string */ public function pluginsPath(string $path = ''): string { return $this->basePath('plugins/' . ltrim($path, '\/')); } /** * Get the path to the application themes directory. * @param string $path path to append * @return string */ public function themesPath(string $path = ''): string { return $this->basePath('themes/' . ltrim($path, '\/')); } /** * Get the path to the application templates (views) directory. * @param string $path path to append * @param bool $adminPanel * @return string */ public function templatesPath(string $path = '', ?bool $adminPanel = null): string { return $this->basePath('tpl/' . ($adminPanel ?? $this->adminPanel() ? 'admin/' : '') . ltrim($path, '\/')); } /** * Запуск приложения * @param \bff\http\Request|null $request * @param bool $respond * @return \bff\http\Response|mixed */ public function run($request = null, bool $respond = true) { # first request if (is_null($request)) { $request = $this->request(); } else { # next request $this->instance('request', $request); } # Admin session config if ($this->adminPanel()) { foreach (config::get('session.admin', []) as $key => $value) { config::temp('session.' . $key, $value); } config::temp('session.path', $request->hostSubdir($this->adminPanel(true))); } # Request params: module + method bff::$class = $this->input()->getpost('s', [TYPE_TEXT, 'len' => 250]); bff::$event = $this->input()->getpost('ev', [TYPE_TEXT, 'len' => 250]); # Search route try { if ($this->adminPanel()) { $route = $this->router()->search([ 'direct' => true, ]); } else { $route = $this->router()->search([ 'direct' => true, 'seo-landing-pages' => true, 'seo-redirects' => true, ]); } } catch (Throwable $e) { $route = null; } # Handle route if ($route) { # Controller/action fallback bff::$class = $route->getControllerName(); bff::$event = $route->getControllerMethod(); # Set request route $request->setRouteResolver(function () use ($route) { return $route; }); } # Call middleware stack $response = $this->middlewareRun($this->finalizeMiddleware( $this->filter('app.middleware', $this->middlewares), $route ), $request); # Fix http protocol mismatch if ($response->getProtocolVersion() !== ($requestProtocol = $request->getProtocolVersion())) { if ($requestProtocol === '2.0') { $requestProtocol = '2'; } $response = $response->withProtocolVersion($requestProtocol); } # Respond if ($respond) { $this->respond($response); } return $response; } /** * Current response object * @param Closure|null $extend * @return \bff\http\Response */ public function response(?Closure $extend = null) { $response = $this['response']; if ($extend instanceof Closure) { $response = $extend($response); $this->instance('response', $response); } return $response; } /** * Send response * @param \bff\http\Response|mixed|null $response * @param bool $finish * @return void */ public function respond($response = null, $finish = false) { Response::responsify($response)->send(); if ($finish) { $this->shutdown(); } } /** * Add middleware to call stack * @param callable|array|string $middleware * @param array $settings [priority, admin, ...] * @return void */ protected function middlewareAdd($middleware, array $settings = []) { if (is_array($middleware) && isset($middleware['callback'])) { $settings = array_merge($settings, $middleware); } else { $settings['callback'] = $middleware; } $this->middlewares[] = $settings; } /** * Finalize middleware stack * @param array $stack * @param Route|null $route * @param array $options [context] * @return array */ public function finalizeMiddleware(array $stack, ?Route $route = null, array $options = []) { # route middleware if ($route) { foreach ($route->getMiddleware() as $middleware) { if (is_string($middleware) && array_key_exists($middleware, $this->middlewareGroups)) { foreach ($this->middlewareGroups[$middleware] as $key => $value) { if (is_string($key)) { $stack[$key] = $value; } else { $stack[] = $value; } } } else { $stack[] = $middleware; } } if ($this->adminPanel()) { # Admin $stack[] = ['callback' => \bff\middleware\AdminPanel::class, 'priority' => 100]; } else { # Frontend ... $stack[] = ['callback' => function (Request $request, $next) use ($route) { # Run $response = $this->router()->runRoute($request, $route); # Html + Layout if (is_string($response)) { return $this->view()->layoutResponse([ 'centerblock' => $this->tagsProcess($response), ]); } # Other response types return Response::responsify($response); }, 'priority' => 100]; } } else { if ($this->adminPanel()) { # Admin $stack[] = ['callback' => \bff\middleware\StartSession::class, 'priority' => 50]; $stack[] = ['callback' => \bff\middleware\AdminPanel::class, 'priority' => 100]; } else { # Not found: Frontend ... $stack[] = function () { return Response::notFound(); }; } } # unify to ['callback' => array|string|Closure, ...] foreach ($stack as $key => $value) { $settings = []; $callback = null; if (is_string($key)) { if (is_int($value)) { # classname => priority $callback = $key; $settings['priority'] = $value; } elseif (is_array($value)) { # classname | name => [settings] $callback = $value['callback'] ?? $key /* classname */; $settings = $value; } elseif (is_string($value)) { # name => classname $callback = $value; } } elseif (is_int($key) && (is_string($value) || $value instanceof Closure)) { # classname || Closure $callback = $value; } elseif (is_array($value)) { if (array_key_exists('callback', $value)) { $callback = $value['callback']; $settings = $value; } else { $callback = $value; } } elseif ($value instanceof Closure || is_object($value)) { # callback $callback = $value; } # context if (! isset($settings['context'])) { $settings['context'] = []; foreach ([static::CONTEXT_FRONTEND, static::CONTEXT_ADMIN, static::CONTEXT_CRON] as $context) { if (isset($settings[$context]) && $settings[$context]) { $settings['context'][] = $context; } } } elseif (! is_array($settings['context'])) { $settings['context'] = [$settings['context']]; } if (! is_null($callback)) { $settings['callback'] = $callback; $stack[$key] = $settings; } else { unset($stack[$key]); } } # route excluded if ($route) { foreach ($route->getMiddlewareExcluded() as $exclude) { foreach ($stack as $key => $middleware) { if ( $middleware['callback'] === $exclude || $key === $exclude ) { unset($stack[$key]); break; } } } } # excluded by context $context = $options['context'] ?? $this->getContext(); if ($context !== false) { foreach ($stack as $key => $middleware) { if (sizeof($middleware['context']) > 0 && !in_array($context, $middleware['context'], true)) { unset($stack[$key]); } } } # sort by priority if (sizeof($stack) > 0) { $i = 1; $order = []; foreach ($stack as $key => $middleware) { $order[$key] = ($middleware['priority'] ?? $i++); } array_multisort($order, SORT_ASC, $stack); } # unique only (remove duplicates) $seen = []; $result = []; foreach ($stack as $middleware) { $key = is_object($middleware['callback']) ? spl_object_id($middleware['callback']) : $middleware['callback']; if (is_array($key)) { $key = join(':', [ (is_object($key[0]) ? spl_object_id($key[0]) : $key[0]), $key[1], ]); } if (! isset($seen[$key])) { $seen[$key] = true; $result[] = $middleware['callback']; } } return $result; } /** * Запуск стека middleware * @param array $pipes * @param mixed $passable * @param Closure|null $destination * @return mixed|\bff\http\Response */ public function middlewareRun(array $pipes, $passable, ?Closure $destination = null) { return (new Pipeline($this)) ->send($passable) ->through($pipes) ->then($destination ?? function ($passable) { return $passable; }); } /** * Обработка вызова метода модуля * @param string $method имя модуля * @param array $parameters аргументы * @return mixed */ public function __call($method, $parameters) { # Call macro method if (static::hasMacro($method)) { return $this->callMacro($method, $parameters); } return null; } /** * Handle dynamic static method calls into the method. * * @param string $method * @param array $parameters * @return mixed */ public static function __callStatic($method, $parameters) { # Call macro method if (static::hasMacro($method)) { return static::callMacroStatic($method, $parameters); } # Static => dynamic return static::getInstance()->$method(...$parameters); } /** * Guess controller by class name * @param string|null $class * @return string */ public function guessControllerByClassName(string $class) { $parts = explode(static::NS, trim($class, ' ' . static::NS)); # Views: {modules|plugins}\controller\... foreach ($parts as $index => $part) { if ($part === 'modules' || $part === 'plugins') { return $parts[$index + 1] ?? ''; } } return ''; } /** * Get controller object by name * @param string $name * @return Module|mixed */ public function resolveController(string $name) { if ($this->moduleExists($name)) { return $this->module($name); } if ($this->pluginExists($name)) { return $this->plugin($name); } if (($theme = $this->theme()) && $theme->getName() === $name) { return $theme; } if ($theme && ($addon = $theme->hasAddon($name))) { return $addon; } if (class_exists($name)) { return $this->make(ltrim($name, static::NS)); } throw new Exception(sprintf('Unable to resolve controller "%s"', $name)); } /** * Вызываем метод требуемого контроллера * @param string $name название контроллера * @param string $method название метода контроллера * @param array $parameters * @param array $opts [inject, direct] * @return mixed */ public function callController(string $name, string $method, array $parameters = [], array $opts = []) { $opts = $this->defaults($opts, [ 'inject' => false, # inject dependencies 'direct' => false, # direct route call ]); $controller = $this->resolveController($name); # Restrict direct call if ($opts['direct']) { # Deny modules in disabled extensions if ( $controller instanceof Module && $controller->extension() && ! $controller->extension()->isActive() ) { return Site::showAccessDenied(); } # Public or allowed method if ($this->frontend()) { if (! $this->security()->isPublicMethod($name, $method)) { return Site::showAccessDenied(); } } elseif ($this->adminPanel()) { $action = $this->input()->getpost('act', TYPE_STR); if ( ! ( $this->security()->haveAccessToModuleMethod($controller, $method, $action) || $this->security()->isPublicMethod($name, $method) ) ) { return Site::showAccessDenied(); } } } if ($opts['inject'] && method_exists($controller, $method)) { # inject dependencies and call existing method return $this->call([$controller, $method], $parameters); } return call_user_func_array([$controller, $method], array_values($parameters)); } /** * Вызываем требуемый метод во всех модулях приложения (в которых он реализован) * @param string $method имя метода * @param array $parameters аргументы * @param array $opts [ * string|null 'context' => контекст вызова * bool 'modules' => вызывать методы модулей * bool 'plugins' => вызывать методы плагинов * bool 'theme' => вызывать методы активной темы * ] * @return void */ public function callModules( string $method, array $parameters = [], array $opts = [] ) { if ($opts['modules'] ?? true) { foreach ($this->getModulesList() as $moduleName => $moduleParams) { $this->module($moduleName)->{$method}(...$parameters); } } if ($opts['plugins'] ?? true) { foreach (Dev::getPluginsList() as $pluginName => $plugin) { $plugin->{$method}(...$parameters); } } if ($opts['theme'] ?? true) { if ($theme = $this->theme()) { $theme->{$method}(...$parameters); } } } /** * Регистрируем дополнительный модуль * @param string $name название модуля * @param string $path путь к директории модуля * @param array $opts [ * string 'class' => название класса модуля или false => $name * bool 'routes' => инициировать роуты модуля * Extension 'extension' => расширение зарегистрировавшее модуль * ] * @return bool */ public function moduleRegister(string $name, string $path, array $opts = []) { $name = $this->unifyModuleName($name); if ( empty($name) || isset($this->modules[$name]) || isset($this->modulesRegistered[$name]) || !is_dir($path) || is_dir($this['path.modules'] . $name) ) { return false; } $opts = $this->defaults($opts, [ 'routes' => true, 'class' => $name, 'extension' => null, 'extension_active' => false, ]); $path = rtrim($path, DS . ' ') . DS; $this->modulesRegistered[$name] = [ 'name' => $name, 'class' => $opts['class'], 'path' => $path, 'extension' => $opts['extension'], 'extension_active' => $opts['extension_active'], ]; if ($opts['routes']) { $this->router()->addMany($this->view()->resolveFileInPath('routes.php', $path)); } return true; } /** * Инициализируем модуль * @param string $name название модуля * @param array $opts [ * string|null 'path' - путь к директории модуля или null (/modules/{name}/) * string|null 'class' - название класса модуля или null => $name * string|null 'context' - application context * bool 'customize' - allow module files customization * bool 'throw' - throw 'Module not found' exception * ] * @return \Module|null */ protected function moduleInit(string $name, array $opts = []) { $name = $this->unifyModuleName($name); $context = $opts['context'] ?? $this->getContext(); if (isset($this->modules[$name][$context])) { return $this->modules[$name][$context]; } $found = false; $customize = $opts['customize'] ?? false; # ищем в модулях приложения (/modules) или по указанному пути $opts['path'] $path = (!empty($opts['path']) ? rtrim($opts['path'], DS) . DS : $this['path.modules'] . $name . DS); $pathContext = modification($path . $name . ($context === static::CONTEXT_ADMIN ? '.adm' : '') . '.class.php', $customize); if (is_file($pathContext)) { $found = true; # [Name]Base require_once modification($path . $name . '.bl.class.php', $customize); # [Name]Model require_once modification($path . $name . '.model.php', $customize); } else { $pathContext = modification($path . $context . '.php', $customize); # fallback cron => frontend if (!is_file($pathContext) && $context === static::CONTEXT_CRON) { $context = static::CONTEXT_FRONTEND; $pathContext = modification($path . $context . '.php', $customize); } if (is_file($pathContext)) { $found = true; # [Name]Base require_once modification($path . 'base.php', $customize); # [Name]Model require_once modification($path . 'model.php', $customize); } } if (! $found) { if ($opts['throw'] ?? true) { throw new Exception(sprintf('Unable to find module "%s"', $name)); } return null; } # extension $extension = $opts['extension'] ?? null; # [Name|NameContext][_] require_once $pathContext; # finalize class name # [Namespace?Class|Name][?_] $class = $opts['class'] ?? $name; $this->classAlias($class); # [ClassContext][_?] $this->classAlias($class . $context); if (class_exists($class . $context, false)) { $class .= $context; } # facade $facade = modification($path . 'facade.php', $customize); if (is_file($facade)) { require_once $facade; } $module = $this->modules[$name][$context] = $this->make($class); $module->setSettings('module_dir', $path); $module->attachExtension($extension); $module->initModule($name, $opts['class'] ?? $name); $module->init(); $this->locale()->translationGroup($name, $module->module_title); $this->hook($name . '.init', $module); return $module; } /** * Был ли модуль инициализирован * @param string $name название модуля * @param string|null $context * @return bool */ public function moduleInited(string $name, ?string $context = null): bool { $name = $this->unifyModuleName($name); return ($context ? isset($this->modules[$name][$context]) : isset($this->modules[$name]) ); } /** * Transform module name to unified form * @param string $name * @return string */ protected function unifyModuleName(string $name) { return mb_strtolower(str_replace(['.', static::DS], '', trim($name))); } /** * Получаем список модулей приложения * @return array */ public function getModulesList(): array { static $cache; # /modules/ if (! isset($cache)) { $modulesList = Files::getDirs($this['path.modules']); foreach ($modulesList as $k => $v) { # ignore modules with special names: .name, _name, test if ($v[0] != '.' && $v[0] != '_' && $v != 'test') { $modulesList[$v] = ['name' => $v, 'path' => $this['path.modules'] . $v . DS]; } unset($modulesList[$k]); } $cache = $modulesList; } # registered foreach ($this->modulesRegistered as $k => $v) { $cache[$k] = $cache[$k] ?? $v; } return $cache; } /** * Возвращаем объект модуля * @param string $name название модуля * @param array $opts * @return \Module|mixed */ public function module(string $name, array $opts = []) { $name = $this->unifyModuleName($name); if (! empty($this->modulesRegistered[$name]['path'])) { $opts = array_merge($opts, $this->modulesRegistered[$name]); } return $this->moduleInit($name, $opts); } /** * Проверяем существование модуля * @param string $name название модуля * @return bool */ public function moduleExists(string $name): bool { return array_key_exists( $this->unifyModuleName($name), $this->getModulesList() ); } /** * Возвращаем объект модели указанного модуля * @param string|object $moduleName название модуля * @param string|null $modelName название модели * @param bool $modelClassOnly только название класса модели * @return \Model|\bff\db\illuminate\Model|mixed|string */ public function model($moduleName, ?string $modelName = null, bool $modelClassOnly = false) { if ($modelName !== null) { $moduleClass = trim(is_object($moduleName) ? get_class($moduleName) : $moduleName, '\_'); if (mb_stripos($moduleClass, static::NS) === false) { $moduleClass = mb_strtolower($moduleClass); if (class_exists(($class = '\modules\\' . $moduleClass . '\models\\' . $modelName))) { return ($modelClassOnly ? $class : new $class()); # app module } if (class_exists(($class = '\bff\modules\\' . $moduleClass . '\models\\' . $modelName))) { return ($modelClassOnly ? $class : new $class()); # core module } if (is_object($moduleName) && $moduleName instanceof Plugin) { $pluginAlias = $moduleName->getAlias(); if (!empty($pluginAlias)) { if (class_exists(($class = '\plugins\\' . $pluginAlias . '\models\\' . $modelName))) { return ($modelClassOnly ? $class : new $class()); # plugin } } } else { if (is_string($moduleName) && $this->pluginExists($moduleName)) { $pluginAlias = $this->plugin($moduleName)->getAlias(); if (!empty($pluginAlias)) { if (class_exists(($class = '\plugins\\' . $pluginAlias . '\models\\' . $modelName))) { return ($modelClassOnly ? $class : new $class()); # plugin } } } } } throw new Exception(sprintf('Unable to find model "%s"', $modelName)); } return $this->module($moduleName)->model; } /** * Determine if the application is running in the admin panel context. * @param bool $pathOnly * @return bool|string */ public function adminPanel(bool $pathOnly = false) { if ($pathOnly) { return $this['admin']->path(true); } return $this->getContext() === static::CONTEXT_ADMIN; } /** * Determine if the application is running in the admin panel context. * @deprecated use adminPanel() * @param bool $pathOnly * @return bool|string */ public function isAdminPanel(bool $pathOnly = false) { return $this->adminPanel($pathOnly); } /** * Switch developers mode * @param bool $enabled */ public function setDevelopersMode(bool $enabled = true) { $this->developersMode = $enabled; } /** * Determine if admin developers mode is enabled. * @deprecated use adminFordev() * @return bool */ public function isAdminFordev(): bool { return $this->adminFordev(); } /** * Determine if admin developers mode is enabled. * @return bool */ public function adminFordev(): bool { return $this->developersMode; } /** * Admin panel theme version * @param string|null $compare * @return string|bool */ public function adminTheme(?string $compare = null) { $theme = $this->config('admin.theme', 'v3'); if ($compare !== null) { return ($theme === $compare); } return $theme; } /** * Admin panel menu manager * @return \bff\admin\Menu */ public function adminMenu() { return $this['admin.menu']; } /** * Determine if the application is running in the frontend context. * @return bool */ public function frontend(): bool { return $this->getContext() === static::CONTEXT_FRONTEND; } /** * Determine if the application is running in cron context. * @return bool */ public function cron(): bool { return $this->getContext() === static::CONTEXT_CRON; } /** * Cron manager component * @return \bff\base\CronManager */ public function cronManager() { return $this['cron']; } /** * Determine if the application is running in the console. * @return bool */ public function isConsole(): bool { return $this->filter('app.console', (mb_stripos(PHP_SAPI, 'cli') === 0)); } /** * Determine if debug mode is enabled. * @return bool */ public function isDebug(): bool { return $this['debug']; } /** * Determine if test mode is enabled. * @return bool */ public function isTest(): bool { return $this['test']; } /** * Determine if localhost mode is enabled. * @return bool */ public function localhost(): bool { return $this['localhost']; } /** * Determine current request context. * @return string */ public function context(): string { return $this->getContext(); } /** * Determine current request context. * @param bool $refresh * @return string */ public function getContext($refresh = false): string { if (is_null($this->context) || $refresh) { if ($this['admin']->isAdminRequest($this->request())) { $this->context = static::CONTEXT_ADMIN; } elseif ($this->isConsole()) { $this->context = static::CONTEXT_CRON; } else { $this->context = $this->filter('app.context', static::CONTEXT_FRONTEND); } } return $this->context; } /** * Set current request context. * @param string $context * @return void */ public function setContext(string $context) { $this->context = $context; } /** * Get config data * @param string|array|null $key * @param mixed $default * @param mixed $filter TYPE_UINT, ... * @return mixed|\bff\base\Config */ public function config($key = null, $default = null, $filter = TYPE_NOCLEAN) { if (is_null($key)) { return $this['config']; } return $this['config']->get($key, $default, $filter); } /** * Cookie prefix * @return string */ public function cookiePrefix(): string { return $this->config('cookie.prefix', 'bff_', TYPE_STR); } /** * Cookie key * @param string $key * @param string|null $prefix * @return string */ public function cookieKey(string $key, ?string $prefix = null): string { return ($prefix ?? $this->cookiePrefix()) . $key; } /** * Демо версия * @return bool */ public function demo() { return defined('BFF_DEMO') || $this->config('demo'); } /** * Errors component * Also available as @see \Errors facade * @return \bff\base\Errors */ public function errors() { return $this['errors']; } /** * Dispatch an event and call the listeners. * @param string|object $event * @param mixed $payload * @param bool $halt * @return array|null */ public function event(...$args) { return Event::dispatch(...$args); } /** * Request component * Also available as @see \Request facade * @return \bff\http\Request */ public function request() { return $this['request']; } /** * Subscribe on new request */ public function onNewRequest($request) { Facade::clearResolvedInstances(); $context = $this->getContext(true); foreach ($this->loggers as $logger) { $logger->onNewRequest($request); } foreach ($this->modules as $instances) { $module = $instances[$context] ?? null; if ($module instanceof contracts\ResetsAfterRequest) { $module->onNewRequest($request); } } if ($theme = $this->theme()) { $theme->onNewRequest($request); } } /** * Router component * Also available as @see \Route facade * @return \bff\base\Router */ public function router() { return $this['router']; } /** * Request input component * Also available as @see \Input facade * @return \bff\base\Input */ public function input() { return $this['input']; } /** * View manager component * Also available as @see \View facade * @return \bff\base\View */ public function view() { return $this['view']; } /** * Locale (languages) component * Also available as @see \Lang facade * @return \bff\base\Locale */ public function locale() { return $this['locale']; } /** * Security component * @return \app\Security */ public function security() { return $this['security']; } /** * Database component * @return \bff\db\Database */ public function database() { return $this['database']; } /** * Hooks manager component * Also available as @see \Hooks facade * @return \bff\extend\Hooks */ public function hooks() { return $this['hooks']; } /** * Hook run * @param string $key ключ хука * @return void */ public function hook(string $key) { $args = func_get_args(); call_user_func_array([$this->hooks(), 'run'], $args); } /** * Subscribe on hook * @param string $key key * @param callable $callable handler * @param int|null $priority priority * @param bool $scoped current request only * @return \bff\extend\Hook|bool */ public function hookAdd(string $key, callable $callable, ?int $priority = null, $scoped = false) { return $this->hooks()->add($key, $callable, $priority, $scoped); } /** * Subscribe on hook during current request only * @param string $key key * @param callable $callable handler * @param int|null $priority priority * @return \bff\extend\Hook|bool */ public function hookAddScoped(string $key, callable $callable, ?int $priority = null) { return $this->hooks()->add($key, $callable, $priority, true); } /** * Subscribe on several hooks * @param array $hooks: * [ * 'hook key 1' => value, * 'hook key 2' => callback, * ] * @return array */ public function hooksAdd(array $hooks = []): array { return $this->hooks()->addMany($hooks); } /** * Check if hook has subscribers * @param string $key hook key * @return bool */ public function hooksAdded(string $key): bool { return $this->hooks()->has($key); } /** * Unsubscribe from hook * @param string $key hook key * @param callable $callable * @return bool */ public function hookRemove(string $key, callable $callable): bool { return $this->hooks()->remove($key, $callable); } /** * Apply filter * @param string $key filter key * @return mixed */ public function filter(string $key) { $args = func_get_args(); return $this->hooks()->apply(...$args); } /** * Применение фильтра c предварительным получением значения из файла системных настроек, * если таковое было указано * @param string $key ключ хука * @return mixed */ public function filterSys(string $key) { $args = func_get_args(); if (isset($args[1])) { $args[1] = $this->config($key, $args[1]); } return $this->hooks()->apply(...$args); } /** * Tags manager * @return \bff\extend\Tags */ public function tags() { return $this['tags']; } /** * Add tag * @param string $id tag ID * @param callable $callable * @return bool */ public function tagAdd(string $id, callable $callable): bool { return $this->tags()->add($id, $callable); } /** * View tag * @param string $id tag ID * @param array $context * @return string */ public function tagView(string $id, $context = []) { return $this->tags()->view($id, $context); } /** * Remove tag * @param string $id tag ID * @return bool */ public function tagRemove(string $id): bool { return $this->tags()->remove($id); } /** * Process tags inside text * @param string $text * @param bool|array $ids tags true - added earlier, array - specified, false - search in text * @param bool $replace replace tags in text, false - return found tags without modifying text * @return mixed */ public function tagsProcess(string $text, $ids = true, bool $replace = true) { return $this->tags()->process($text, $ids, $replace); } /** * Возвращаем объект плагина * @param string $name название плагина * @return Plugin|bool */ public function plugin(string $name) { return Dev::getPlugin($name); } /** * Проверяем был ли подключен плагин * @param string $name название плагина * @return bool */ public function pluginExists(string $name): bool { return ($this->plugin($name) !== false); } /** * Модуль Dev * @return \Dev */ public function dev() { return $this->module('dev'); } /** * Проверка на index-страницу * todo * @return bool */ public function isIndex() { return empty(bff::$class) || (bff::$class == 1); } /** * Определение типа устройства * @param string|null $compare тип определяемого устройства или null * @return bool|string */ public function deviceDetector(?string $compare = null) { if (is_null($compare)) { return $this->request()->device(); } return $this->request()->isDevice($compare); } /** * Init request device */ protected function initDevice() { $request = $this->request(); /** @deprecated use Request::isDesktop() */ define('DEVICE_DESKTOP', $request->isDesktop()); /** @deprecated use Request::isTablet() */ define('DEVICE_TABLET', $request->isTablet()); /** @deprecated use Request::isPhone() */ define('DEVICE_PHONE', $request->isPhone()); } /** * Список устройств * @return array */ public function devices(): array { $data = [ static::DEVICE_DESKTOP => [ 't' => _t('', 'desktop'), ], static::DEVICE_PHONE => [ 't' => _t('', 'mobile'), ], ]; foreach ($data as $k => &$v) { $v['id'] = $k; } unset($v); return $this->filter('app.devices', $data); } /** * Работает ли приложение в https-only режиме * @param bool $requestOnly протокол текущего запроса * @return bool */ public function httpsOnly(bool $requestOnly = false): bool { return $this->request()->scheme($requestOnly) === 'https'; } /** * Отправка письма на основе шаблона * @param array $tplVars данные подставляемые в шаблон * @param string $tplName ключ шаблона письма * @param string $to email получателя * @param string|null $subject заголовок письма или null|false (берем из шаблона письма) * @param string $from email отправителя * @param string $fromName имя отправителя * @param string|null $lang ключ языка шаблона * @param array $opts доп. параметры ['headers' => заголовки письма] * @return bool */ public function sendMailTemplate( array $tplVars, string $tplName, string $to, ?string $subject = null, string $from = '', string $fromName = '', ?string $lang = null, array $opts = [] ): bool { try { $lang = $lang ?? $this->locale()->current(); $data = Sendmail::getMailTemplate($tplName, $tplVars, $lang); if (!empty($subject)) { $data['subject'] = $subject; } $data['name'] = $tplName; $data['to'] = $to; $data['from'] = $from; $data['fromName'] = $fromName; $data['vars'] = &$tplVars; $data['lang'] = $lang; $data['opts'] = &$opts; $unsubscribe = ''; if (! isset($opts['headers']['List-Unsubscribe'])) { if (! isset($tplVars['unsubscribe'])) { $unsubscribe = Sendmail::getUnsubscribeUrl($tplVars['user_id'] ?? 0); } else { $unsubscribe = $tplVars['unsubscribe']; } } if ($unsubscribe) { $opts['headers']['List-Unsubscribe'] = '<' . $unsubscribe . '>'; } $data = $this->filter('mail.send.template', $data, $tplName); if ($this->localhost()) { $this->log(['tpl' => $tplName, 'data' => $data]); $this->filter('mail.send', $data); return true; } if (is_bool($data)) { return $data; } if (empty($data['template'][Sendmail::CHANNEL_EMAIL]['enabled'])) { return true; } return $this->sendMail( $data['to'], $data['subject'], $data['body'], $data['from'], $data['fromName'], $opts ); } catch (Throwable $e) { $this->errors()->set('sendMailTemplate: ' . $tplName, true); $this->errors()->set($e->getMessage(), true); } return false; } /** * Отправка письма * @param string $to email получателя * @param string $subject заголовок письма * @param string $body тело письма * @param string $from email отправителя * @param string $fromName имя отправителя * @param array $opts доп. параметры ['headers' => заголовки письма] * @return bool */ public function sendMail( string $to, string $subject, string $body, string $from = '', string $fromName = '', array $opts = [] ): bool { return Sendmail::sendMail($to, $subject, $body, $from, $fromName, $opts); } /** * E-mail адрес администратора * @return string */ public function mailAdmin(): string { return $this->config('mail.admin', 'admin@' . SITEHOST, TYPE_NOTAGS); } /** * E-mail адрес для отправки уведомлений не требующих ответа * @return string */ public function mailNoreply(): string { return $this->config('mail.noreply', 'noreply@' . SITEHOST, TYPE_NOTAGS); } /** * Отправка SMS на основе шаблона * @param array $tplVars данные подставляемые в шаблон * @param string $tplName ключ шаблона * @param array $opts параметры ['lang', 'phoneNumber'] * @return bool */ public function sendSmsTemplate(array $tplVars, string $tplName, array $opts = []): bool { $opts = $this->defaults($opts, [ 'lang' => $this->locale()->current(), 'phoneNumber' => '', ]); do { $data = Sendmail::getSmsTemplate($tplName, $tplVars, $opts['lang']); if (empty($data['template'])) { break; } if (empty($data['template']['enabled'])) { break; } if (! empty($tplVars['user_id'])) { $user = Users::model()->userData( $tplVars['user_id'], ['phone_number', 'phone_number_verified', 'enotify_sms'] ); if (empty($user['phone_number'])) { break; } if (empty($data['template']['force_verified']) && empty($user['phone_number_verified'])) { break; } if (! empty($data['template']['enotify']) && ! ($data['template']['enotify'] & $user['enotify_sms'])) { break; } $opts['phoneNumber'] = $user['phone_number']; } $data = $this->filter('sms.send.template', $data, $tplVars, $tplName, $opts); if (is_bool($data)) { return $data; } if (empty($opts['phoneNumber'])) { break; } if (empty($data['body'])) { break; } return Users::i()->sms()->send($opts['phoneNumber'], $data['body']); } while (false); return false; } /** * Формирование директорий миграций * @return array */ public function migrationsPaths(): array { $paths = [ 'migrations' => [], 'seeds' => [], ]; $paths['migrations'][] = $this->basePath('files/migrations'); foreach ($paths['migrations'] as $path) { if (is_dir($path . DS . 'seeds')) { $paths['seeds'][] = $path . DS . 'seeds'; } } return $this->filter('app.migrations.paths', $paths); } /** * Формирование пути к файлу * @param string $part вложенная директория либо относительный путь к файлу * @param string|null $type тип пути, доступные типы: 'images' * @param array $opts [notheme, nocustom] * @return string */ public function path(string $part, ?string $type = null, array $opts = []): string { # public /files/images/{dir} if ($type === 'images') { return $this->publicPath('files/images/' . (!empty($part) ? $part . '/' : '')); } # public /files/ if (empty($part)) { return $this->publicPath('files/'); } elseif ($part[0] !== '/') { # public /files/{dir}/ return $this->publicPath('files/' . $part . '/'); } # file path $file = $this->basePath($part); if (empty($opts['notheme'])) { $themed = $this->view()->resolveFileInTheme($part); if ($themed !== false) { $file = $themed; } } if (empty($opts['nocustom'])) { $file = modification($file); } return $file; } /** * Формирование URL для статики * @param string $part часть URL * @param string|null $version версия или 'images' * @param array $opts [relative, dynamic, scheme, notheme, nocustom] * @return string */ public function url(string $part, ?string $version = null, array $opts = []): string { # static url $relative = !empty($opts['relative']); $static = (!$relative ? SITEURL_STATIC : ''); if (! empty($opts['dynamic'])) { $static = '//' . Url::HOST_PLACEHOLDER; } if (! empty($opts['scheme']) && !$relative && $static[0] === '/') { $static = (is_string($opts['scheme']) ? $opts['scheme'] : $this->request()->scheme()) . ':' . $static; } # public /files/images/{dir?} if ($version === 'images') { return $static . '/files/images/' . ($part !== '' ? $part . '/' : ''); } # public /files/ if (empty($part)) { return $static . '/files/'; } elseif ($part[0] !== '/') { # public /files/{dir} return $static . '/files/' . $part . '/'; } # public file url $file = $static . $part; # themed if (empty($opts['notheme'])) { $themed = $this->view()->resolveFileInTheme($part, true, [ 'version' => &$version, # change to mix-manifest hash ]); if ($themed !== false) { $file = $themed; } } # /public/custom/file if (empty($opts['nocustom'])) { $fileRelative = mb_substr($file, mb_strlen($static)); if (is_file($this->publicPath('custom' . $fileRelative))) { $file = $static . '/custom' . $fileRelative; # use custom file } } # version $version = (!empty($version) ? (stripos($version, '?') === false ? '?v=' . $version : $version) : ''); return $file . $version; } /** * Формирование полного URL для статики * @param string $url относительный URL * @param string $version версия * @return string */ public function urlStatic(string $url, string $version = ''): string { return $this->url($url, $version); } /** * Формирование базового URL * @deprecated use Url::to() * @param bool $trailingSlash * @param string|null $lang ключ языка * @param array $subdomains поддомены * @return string */ public function urlBase( bool $trailingSlash = true, ?string $lang = null, array $subdomains = [] ): string { return Url::to(($trailingSlash ? '/' : ''), [ 'lang' => $lang, 'subdomains' => $subdomains, ]); } /** * Формирование ajax URL * @param string $moduleName название модуля * @param string $actionQuery доп. параметры запроса * @return string */ public function ajaxURL(string $moduleName, string $actionQuery): string { return '/index.php?bff=ajax&s=' . $moduleName . '&act=' . $actionQuery; } /** * Текущая тема * @param string|bool $active true - текущая тема, 'name' - название темы (для инициализации) * @param bool $testMode приоритет у темы с включенным режимом тестирования * @return \Theme */ public function theme($active = true, bool $testMode = true) { return Dev::themeInit($active, false, $testMode); } /** * Логирование сообщение * @param string|array $message сообщение * @param string|int|array|bool $level уровень логирования, true - добавить к ajax ответу * @param string $name название логгера * @param array $context данные контекста * @return void */ public function log($message, $level = Logger::ERROR, $name = Logger::DEFAULT_FILE, array $context = []) { if (is_bool($level) && $this->isDebug()) { $level = Logger::ERROR; if (is_array($name) && empty($context)) { $context = $name; $name = Logger::DEFAULT_FILE; } $this->logger($name)->console($level, $message, $context); } if (is_array($message)) { $message = print_r($message, true); } if (is_string($level) && mb_stripos($level, '.log') !== false) { $name = $level; $level = Logger::ERROR; } if (is_array($level)) { foreach ($level as $k => $v) { $context[$k] = $v; } $level = Logger::ERROR; } $this->logger($name)->log($level, $message, $context); } /** * Инициализируем объект логгера * @param string $name имя логгера * @param string|null $fileName путь к файлу * @param int|bool $level * @param array $handlers * @param array $processors * @return Logger */ public function logger( string $name = Logger::DEFAULT_FILE, ?string $fileName = null, $level = false, array $handlers = [], array $processors = [] ) { if (isset($this->loggers[$name])) { return $this->loggers[$name]; } # Rotating file logger if (!empty($fileName) || empty($handlers)) { $logger = Logger::factoryRotatingFile($name, ($fileName ?: $name), $level, false, $handlers, $processors); } else { # Other logger handlers $logger = new Logger($name, $handlers, $processors); } return ($this->loggers[$name] = $logger); } /** * Установка meta тегов * @param string|null $title заголовок страницы * @param string|null $keywords ключевые слова * @param string|null $description описание * @param array $macrosData данные для макросов * @param bool $last true - окончательный вариант, false - перекрываемый более поздним вызовом setMeta * @return void */ public function setMeta( ?string $title = null, ?string $keywords = null, ?string $description = null, array $macrosData = [], bool $last = true ) { static $set = false; # todo if ($set === true && !$last) { return; } if ($last) { $set = true; } # заменяем макросы $data = []; if (!empty($title)) { $data['mtitle'] = $title; } if (!empty($keywords)) { $data['mkeywords'] = $keywords; } if (!empty($description)) { $data['mdescription'] = $description; } $data = SEO::metaTextPrepare($data, $macrosData); # устанавливаем meta теги foreach ($data as $k => &$v) { if (empty($v)) { continue; } SEO::metaSet($k, $v); config::temp($k . '_' . $this->locale()->current(), trim($v, ' -|,')); # old version compability } unset($v); } /** * Состояние системы * @return \bff\base\HH */ public function hh() { return HH::i(); } /** * Метод, вызываемый перед завершением запроса * @return void */ public function shutdown() { $this->hook('app.shutdown'); exit; } /** * Создаем псевдоним класса class_ => class * @param string $class имя класса * @param string|null $original исходное имя класса или null = 'class_' * @return bool */ public function classAlias(string $class, ?string $original = null) { if (class_exists($class, false)) { return false; } $original = mb_strtolower($original ?: $class . '_'); if (! class_exists($original, false)) { return false; } if (! empty($this->classExtensions[$original])) { foreach ($this->classExtensions[$original] as $extension) { class_alias($original, $extension['base']); if (is_callable($extension['path'])) { $extension['className'] = $class; $extension['originalName'] = $original; call_user_func($extension['path'], $extension); } else { include_once $extension['path']; } $original = $extension['name']; } } return class_alias($original, $class); } /** * Расширение класса * @param string $class имя исходного класса * @param string $extension имя класса расширения * @param string|callable $file путь к файлу объявляющему класс расширения или callback * @param string|null $context контекст запуска или null (любой) * @return bool */ public function classExtension(string $class, string $extension, $file, ?string $context = null) { if (empty($class) || empty($extension)) { return false; } if (!is_callable($file) && !file_exists($file)) { return false; } if ($context && $context !== $this->getContext()) { return false; } $this->classExtensions[mb_strtolower($class)][] = [ 'base' => $extension . '_Base', 'name' => $extension, 'path' => $file, ]; return true; } /** * Autoload * @param string $class name to load * @return void */ public function autoload($class) { $log = ''; do { $class = ltrim($class, static::NS); if (isset($this->autoloadMap[$class])) { if (is_string($this->autoloadMap[$class])) { # alias, facade if (isset($this->autoloadMap[$class])) { class_alias($this->autoloadMap[$class], $class); $log = '1) autoload map (as alias "' . $class . '")'; break; } } else { # registered component (core, app) list($group, $path) = $this->autoloadMap[$class]; if (in_array($group, ['core', 'app'], true)) { $path = $this[($group === 'core' ? 'path.core' : 'path.base')] . $path; if (file_exists($path)) { include modification($path); $aliasOf = $this->autoloadMap[$class]['aliasof'] ?? null; $this->classAlias($class, $aliasOf); $log = join('', [ '2) autoload map in "' . $group . '"', ($aliasOf ? ' (alias of ' . $aliasOf . ')' : ''), ]); break; } } } break; } # alias: class_ => class if (class_exists($class . '_', false)) { $this->classAlias($class); $log = '3) alias of "' . $class . '_" => "' . $class . '"'; break; } # try to load application module if (mb_strpos($class, static::NS) === false) { $this->module($class, ['throw' => false]); $log = '4) load module'; break; } # class with "theme" support $themed = ( # \views\ mb_stripos($class, static::NS . 'views' . static::NS) !== false ); # namespace prefixed class => file path if ($this->autoloadByNamespace($class, $themed)) { $log = '5) class => namespace prefix'; break; } # namespaced class => file path $path = str_replace(static::NS, static::DS, $class) . '.php'; if ($themed) { # themed version if ($themePath = $this->view()->resolveFileInTheme(static::DS . $path)) { include modification($themePath); $this->classAlias($class); $log = '6) class => themed path'; break; } } foreach ([$path, mb_strtolower($path)] as $path) { if (is_file($this['path.base'] . $path)) { include modification($this['path.base'] . $path); $this->classAlias($class); $log = '7) class => path'; break 2; } } $log = '8) skip'; } while (false); } /** * Autoload class by matched namespace prefix * @param string $class without leading \ * @param bool $themed version allowed * @return bool */ protected function autoloadByNamespace(string $class, bool $themed = false) { foreach ($this->autoloadNamespaces as $namespace => $path) { if (mb_stripos($class, $namespace) === 0) { $files = [ # \namespace\class => /path/class.php str_replace(static::NS, static::DS, str_replace($namespace . static::NS, $path, $class)) . '.php' ]; # themed version if ($themed) { # /path/class.php => /path/_theme/class.php array_unshift( $files, str_replace($path, $path . '_' . $this->theme()->getName() . static::DS, $files[0]) ); } foreach ($files as $file) { if (file_exists($file)) { include modification($file); $this->classAlias($class); return true; } } } } return false; } /** * Register autoload class by namespace prefix * @param string $namespace prefix * @param string $path * @return bool */ public function autoloadNamespace(string $namespace, string $path) { $namespace = trim($namespace, static::NS); $path = rtrim($path, static::DS) . static::DS; if ($namespace && $path) { # prepend: last added has higher priority $this->autoloadNamespaces = [$namespace => $path] + $this->autoloadNamespaces; return true; } return false; } /** * Расширяем autoload * @param array $classes : * [ * 'имя класса' => ['ключ группы, варианты: [app, core]', 'путь к файлу, относительно директории группы', ...] * ] * @return void */ public function autoloadAdd(array $classes) { foreach ($classes as $k => $v) { $this->autoloadMap[$k] = $v; } } }