yf_services classAbstraction layer over YF services.
| File | classes/yf_services.class.php |
|---|---|
| Use | _class('services') |
A site sets them in its config with $PROJECT_CONF['services']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$ABANDONED_NOTICEstring|callable |
silent by default, same values as CLONE_DEPRECATION */
Property [ public $ABANDONED_NOTICE = '' |
channel of the "abandoned upstream" notice (would repeat on every request): '' = silent by default, same values as CLONE_DEPRECATION |
$ALLOW_GIT_CLONEbool |
true |
false: a loader that would clone or run the embedded composer throws RuntimeException instead; true keeps the old behaviour ($PROJECT_CONF['services']['ALLOW_GIT_CLONE']) |
$ALLOW_RUNTIME_FETCHbool |
false |
with USE_LOCKS: fetch a missing class file of a locked package from raw.githubusercontent.com at the locked sha on first use; off: a missing file is not loaded ($PROJECT_CONF['services']['ALLOW_RUNTIME_FETCH']) |
$CLONE_DEPRECATIONstring|callable |
error_log(), 'trigger' = trigger_error(E_USER_DEPRECATED), '' = silent, Closure or [object, method] = called with the message */
Property [ public $CLONE_DEPRECATION = 'log' |
where the clone/abandoned notices go: 'log' = error_log(), 'trigger' = trigger_error(E_USER_DEPRECATED), '' = silent, Closure or [object, method] = called with the message |
$USE_LOCKSbool |
false |
true: a loader that has services/<name>.lock.json is served by services_autoload (psr-4 / psr-0 / classmap index from the lock) instead of its git clone / composer path; default flips in 4.0 ($PROJECT_CONF['services']['USE_LOCKS']) |
$VENDOR_DIRstring |
APP_PATH . 'vendor/'; the framework dir is never written to after deploy, libraries an older version fetched into YF_PATH/vendor/ are still read from there ($PROJECT_CONF['services']['VENDOR_DIR'])... |
vendor dir that run-time and services:fetch downloads of the framework's loaders go to, '' = APP_PATH . 'vendor/'; the framework dir is never written to after deploy, libraries an older version fetched into YF_PATH/vendor/ are still read from there ($PROJECT_CONF['services']['VENDOR_DIR']) |
static $_current_libstring |
'' |
name of the library being loaded by require_php_lib(), read by services/_yf_autoloader.php for its notices |
static $_installed_cachearray |
[] |
package names per vendor dir, read from composer/installed.json |
static $_notifiedarray |
[] |
notices already sent, one per message |
$_paths_cache |
null |
|
$php_libs |
[] |
public function base58_decode($in)
public function base58_decode($in)
{
$this->require_php_lib('base58');
$base58 = new StephenHill\Base58();
return $base58->decode($in);
}
public function base58_encode($in)
public function base58_encode($in)
{
$this->require_php_lib('base58');
$base58 = new StephenHill\Base58();
return $base58->encode($in);
}
public function coffee($content, $params = [])
public function coffee($content, $params = [])
{
$this->require_php_lib('coffeescript');
return \CoffeeScript\Compiler::compile($content, ['header' => false]);
}
public function google_translate($text, $lang_from, $lang_to, $params = [], &$cache_used = false)
public function google_translate($text, $lang_from, $lang_to, $params = [], &$cache_used = false)
{
if ( ! strlen($text) || ! $lang_from || ! $lang_to) {
return false;
}
$md5 = md5($lang_from . '|' . $lang_to . '|' . $text);
$table = 'cache_google_translate';
$cached = db()->from($table)->where('lang_from', $lang_from)->where('lang_to', $lang_to)->where('md5', $md5)->get();
if (isset($cached['translated'])) {
$cache_used = true;
return $cached['translated'];
}
$this->require_php_lib('google_translate');
$translated = null;
try {
$translated = Stichoza\GoogleTranslate\TranslateClient::translate($lang_from, $lang_to, $text);
} catch (Exception $e) {
echo 'Error: exception caught: ' . $e->getMessage() . PHP_EOL;
}
db()->insert_safe($table, [
'md5' => $md5,
'lang_from' => $lang_from,
'lang_to' => $lang_to,
'source' => $text,
'translated' => $translated,
'date' => date('Y-m-d H:i:s'),
]);
return $translated;
}
public function haml($content, $params = [])
Process and output HAML content.
public function haml($content, $params = [])
{
$this->require_php_lib('mthaml');
$haml = new MtHaml\Environment('php');
$executor = new MtHaml\Support\Php\Executor($haml, [
'cache' => sys_get_temp_dir() . '/haml',
]);
$path = tempnam(sys_get_temp_dir(), 'haml');
file_put_contents($path, $content);
return $executor->render($path, $params);
}
public function jade($content, $params = [])
Process and output JADE content.
public function jade($content, $params = [])
{
$this->require_php_lib('jade');
$dumper = new \Everzet\Jade\Dumper\PHPDumper();
$parser = new \Everzet\Jade\Parser(new \Everzet\Jade\Lexer\Lexer());
$jade = new \Everzet\Jade\Jade($parser, $dumper);
return $jade->render($content);
}
public function less($content, $params = [])
public function less($content, $params = [])
{
$this->require_php_lib('less');
$less = new \lessc();
return $less->compile($content);
}
public function markdown($content, $params = [])
public function markdown($content, $params = [])
{
$input = null;
$this->require_php_lib('parsedown');
$parsedown = new Parsedown();
return $parsedown->text($content);
}
public function phpmailer($content, $params = [])
phpmailer fresh instance, intended to use its helper methods.
public function phpmailer($content, $params = [])
{
$this->require_php_lib('phpmailer');
return new PHPMailer(true);
}
public function require_php_lib($name, $params = [])
public function require_php_lib($name, $params = [])
{
if (isset($this->php_libs[$name])) {
return $this->php_libs[$name];
}
if ( ! isset($this->_paths_cache)) {
$suffix = '.php';
$patterns = [
'framework' => [
YF_PATH . 'services/*' . $suffix,
YF_PATH . 'services/*/*' . $suffix,
YF_PATH . 'share/services/*' . $suffix,
YF_PATH . 'share/services/*/*' . $suffix,
YF_PATH . 'plugins/*/services/*' . $suffix,
YF_PATH . 'plugins/*/services/*/*' . $suffix,
YF_PATH . 'plugins/*/share/services/*' . $suffix,
YF_PATH . 'plugins/*/share/services/*/*' . $suffix,
],
'app' => [
APP_PATH . 'services/*' . $suffix,
APP_PATH . 'services/*/*' . $suffix,
APP_PATH . 'share/services/*' . $suffix,
APP_PATH . 'share/services/*/*' . $suffix,
APP_PATH . 'plugins/*/services/*' . $suffix,
APP_PATH . 'plugins/*/services/*/*' . $suffix,
APP_PATH . 'plugins/*/share/services/*' . $suffix,
APP_PATH . 'plugins/*/share/services/*/*' . $suffix,
],
];
$slen = strlen($suffix);
$paths = [];
foreach ($patterns as $gname => $pathsList) {
foreach ($pathsList as $glob) {
foreach (glob($glob) as $_path) {
$_name = substr(basename($_path), 0, -$slen);
if ($_name == '_yf_autoloader') {
continue;
}
$paths[$_name] = $_path;
}
}
}
// Ensures services with the same name can be overridden inside the project
foreach ($paths as $_name => $_path) {
$this->_paths_cache[$_name] = $_path;
}
}
$path = $this->_paths_cache[$name] ?? '';
if ( ! $path || ! file_exists($path)) {
throw new Exception('services ' . __FUNCTION__ . ' "' . $name . '" not found');
return false;
}
$lib_config = (static function ($path, $return_config) {
return include $path;
})($path, true);
if (is_array($lib_config)) {
if (isset($lib_config['abandoned'])) {
self::_notify('services: "' . $name . '" is abandoned upstream: ' . $lib_config['abandoned'] . '; yf 4.0 removes the loader', _class('services')->ABANDONED_NOTICE);
}
$noop = $lib_config['noop'] ?? false;
if (($noop && PHP_VERSION_ID >= 80200 && ($noop === true || extension_loaded($noop))) || self::_composer_provided($lib_config)) {
// a composer-provided lib still needs the services its loader chains (dom-crawler → css-selector)
foreach ((array) ($lib_config['require_services'] ?? []) as $service) {
$this->require_php_lib($service);
}
$this->php_libs[$name] = $path;
return '';
}
}
if ($this->USE_LOCKS && is_array($lib_config) && $this->_require_from_lock($name, $lib_config)) {
$this->php_libs[$name] = $path;
return '';
}
$return_config = false;
$prev_lib = self::$_current_lib;
self::$_current_lib = $name;
ob_start();
try {
require $path; // not require_once: the config include above already registered the file
} finally {
self::$_current_lib = $prev_lib;
$out = ob_get_clean();
}
$this->php_libs[$name] = $path;
return $out;
}
public function sass($content, $params = [])
public function sass($content, $params = [])
{
$this->require_php_lib('scss');
$scss = new scssc();
return $scss->compile($content);
}
_ hooks and helpers; not routable as page actionspublic function __call($name, $args)
Catch missing method call.
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
public function __clone()
We cleanup object properties when cloning.
public function __clone()
{
foreach ((array) get_object_vars($this) as $k => $v) {
$this->$k = null;
}
}
public function __toString(): string
Need to avoid calling render() without params.
public function __toString()
{
try {
return (string) $this->render();
} catch (Exception $e) {
return '';
}
}
public static function _before_clone($lib, $source)
Called by services/_yf_autoloader.php right before it fetches a library at run time.
$source mixed composer package or git url to name in the message public static function _before_clone($lib, $source)
{
$message = 'services: "' . $lib . '" is fetched by git clone at run time; add "' . $source . '" to your composer.json (yf 4.0 drops the clone path)';
if ( ! _class('services')->ALLOW_GIT_CLONE) {
throw new RuntimeException(str_replace('is fetched by git clone at run time', 'needs a git clone at run time and ALLOW_GIT_CLONE is off', $message));
}
self::_notify($message);
}
public static function _composer_installed($package, $vendor_dirs = null)
Is a composer package installed in any vendor dir whose autoload.php was loaded by yf_main::init_composer_autoload()?
$package mixed "vendor/package", a ":constraint" suffix is ignored$vendor_dirs mixed list of vendor dirs, default: those of the loaded autoloaders public static function _composer_installed($package, $vendor_dirs = null)
{
$package = strtolower(explode(':', (string) $package)[0]);
if ($vendor_dirs === null) {
$vendor_dirs = [];
$main = $GLOBALS['main'] ?? null;
foreach ((array) ($main->_composer_autoload_loaded ?? []) as $autoload) {
$vendor_dirs[] = dirname($autoload);
}
}
foreach ((array) $vendor_dirs as $dir) {
$dir = rtrim($dir, '/') . '/';
if ( ! isset(self::$_installed_cache[$dir])) {
$names = [];
$file = $dir . 'composer/installed.json';
$data = is_file($file) ? json_decode((string) file_get_contents($file), true) : null;
// composer 2: {"packages": [...]}, composer 1: [...]
foreach ((array) (is_array($data) && isset($data['packages']) ? $data['packages'] : $data) as $item) {
if (is_array($item) && isset($item['name'])) {
$names[strtolower($item['name'])] = true;
}
}
self::$_installed_cache[$dir] = $names;
}
if (isset(self::$_installed_cache[$dir][$package])) {
return true;
}
}
return false;
}
public static function _composer_provided($config)
Is the library of a loader config already provided by composer (installed package or loadable check_class)?
public static function _composer_provided($config)
{
if ( ! empty($config['check_class']) && class_exists($config['check_class'], true)) {
return true;
}
foreach ((array) ($config['composer_names'] ?? []) as $package) {
if (self::_composer_installed($package)) {
return true;
}
}
return false;
}
public static function _notify($message, $channel = null)
Send a notice (once per message) to the channel set in $CLONE_DEPRECATION.
public static function _notify($message, $channel = null)
{
$channel = $channel ?? _class('services')->CLONE_DEPRECATION;
if ( ! $channel || isset(self::$_notified[$message])) {
return false;
}
self::$_notified[$message] = true;
if ($channel === 'trigger') {
trigger_error($message, E_USER_DEPRECATED);
} elseif ($channel instanceof Closure || (is_array($channel) && is_callable($channel))) {
$channel($message);
} else {
error_log($message);
}
return true;
}
public static function _project_vendor_dir(): string
The project's vendor dir for libraries fetched at run time: $VENDOR_DIR, else APP_PATH/vendor/; YF_PATH/vendor/ only when no APP_PATH is defined (a service file run on its own).
public static function _project_vendor_dir(): string
{
$dir = function_exists('_class') ? (string) (_class('services')->VENDOR_DIR ?? '') : '';
if ($dir === '') {
$dir = (defined('APP_PATH') && strlen((string) APP_PATH) ? APP_PATH : YF_PATH) . 'vendor/';
}
return rtrim($dir, '/') . '/';
}
public function _require_from_lock($name, $config)
Serve a loader from its services/<name>.lock.json through services_autoload, the clone / composer path is skipped.
Returns bool false: no usable lock or a loader entry that cannot be mapped, the caller takes the old path
public function _require_from_lock($name, $config)
{
$lock_class = _class('services_lock');
$lock_path = $lock_class->lock_path($name);
$lock = $lock_class->read($lock_path);
if ( ! $lock) {
return false;
}
$converted = ($lock['converted_from'] ?? '') === 'loader';
$root_package = (string) key((array) ( ! empty($lock['roots']) ? $lock['roots'] : $lock['packages']));
[$vendor_dir, $fallback_dir] = self::_vendor_dirs(dirname($lock_path, 2));
// [package, path] of the files the loader requires itself: a converted lock lists them in its `files`, otherwise the
// require_once entries are relative to the clone root (YF_PATH/vendor/): <lib dir>/file -> <vendor>/<package>/file
$lib_dirs = array_merge(array_values((array) ($config['git_urls'] ?? [])), array_keys((array) ($config['autoload_config'] ?? [])));
$files = [];
foreach ($converted ? [] : (array) ($config['require_once'] ?? []) as $file) {
$rel = null;
foreach ($lib_dirs as $lib_dir) {
$lib_dir = rtrim((string) $lib_dir, '/') . '/';
if (strncmp((string) $file, $lib_dir, strlen($lib_dir)) === 0) {
$rel = substr((string) $file, strlen($lib_dir));
break;
}
}
if ($rel === null || $root_package === '') {
return false;
}
$files[] = [$root_package, $rel];
}
foreach ($converted ? (array) $lock['packages'] : [] as $package => $entry) {
foreach ((array) ($entry['autoload']['files'] ?? []) as $rel) {
$files[] = [(string) $package, (string) $rel];
}
}
if ($files && ! ($this->ALLOW_RUNTIME_FETCH || _class('services_autoload')->ALLOW_RUNTIME_FETCH)) {
foreach ($files as [$package, $rel]) {
if ( ! is_file($vendor_dir . $package . '/' . $rel) && ! ($fallback_dir !== '' && is_file($fallback_dir . $package . '/' . $rel))) {
trigger_error('services: "' . $name . '" lists ' . $rel . ' in ' . ($converted ? 'its lock' : 'require_once') . ', it is not in ' . $vendor_dir . $package . '/ and ALLOW_RUNTIME_FETCH is off, the loader keeps its old path', E_USER_NOTICE);
return false;
}
}
}
foreach (array_diff(array_unique(array_merge((array) ($config['require_services'] ?? []), (array) ($lock['services'] ?? []))), (array) ($lock['inlined_services'] ?? [])) as $service) {
$this->require_php_lib($service);
}
$auto = _class('services_autoload');
$auto->ALLOW_RUNTIME_FETCH = $auto->ALLOW_RUNTIME_FETCH || $this->ALLOW_RUNTIME_FETCH;
$auto->register_lock($name, $lock, $vendor_dir, $fallback_dir);
foreach ($converted ? [] : $files as [$package, $rel]) {
$auto->include_file($package, $rel);
}
if (is_callable($config['manual'] ?? null)) {
$config['manual'](new class($vendor_dir) {
public $libs_root;
public function __construct($libs_root)
{
$this->libs_root = $libs_root;
}
});
}
return true;
}
public static function _vendor_dirs($root): array
[write dir, read-only fallback dir] for the packages of a lock under $root: the framework's locks write into the project vendor dir and still read files an older version fetched into YF_PATH/vendor/; any other root keeps <root>/vendor/.
$root string dir that holds services/<name>.lock.json public static function _vendor_dirs($root): array
{
$root = rtrim((string) $root, '/') . '/';
$own = $root . 'vendor/';
$real_root = realpath($root);
if ($real_root === false || $real_root !== realpath(YF_PATH)) {
return [$own, ''];
}
$project = self::_project_vendor_dir();
$same = rtrim(realpath($project) ?: $project, '/') === rtrim(realpath($own) ?: $own, '/');
return [$project, $same ? '' : $own];
}