yf_services class

Abstraction layer over YF services.

Fileclasses/yf_services.class.php
Use_class('services')

Public properties

A site sets them in its config with $PROJECT_CONF['services']['NAME'] = ...;

PropertyDefaultDescription
$ABANDONED_NOTICE
string|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_CLONE
bool
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_FETCH
bool
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_DEPRECATION
string|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_LOCKS
bool
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_DIR
string
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_lib
string
'' name of the library being loaded by require_php_lib(), read by services/_yf_autoloader.php for its notices
static $_installed_cache
array
[] package names per vendor dir, read from composer/installed.json
static $_notified
array
[] notices already sent, one per message
$_paths_cache null
$php_libs []

Public methods 21

base58_decode

public function base58_decode($in)
    public function base58_decode($in)
    {
        $this->require_php_lib('base58');
        $base58 = new StephenHill\Base58();
        return $base58->decode($in);
    }

base58_encode

public function base58_encode($in)
    public function base58_encode($in)
    {
        $this->require_php_lib('base58');
        $base58 = new StephenHill\Base58();
        return $base58->encode($in);
    }

coffee

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;
    }

haml

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);
    }

jade

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);
    }

less

public function less($content, $params = [])
    public function less($content, $params = [])
    {
        $this->require_php_lib('less');
        $less = new \lessc();
        return $less->compile($content);
    }

markdown

public function markdown($content, $params = [])
    public function markdown($content, $params = [])
    {
        $input = null;
        $this->require_php_lib('parsedown');
        $parsedown = new Parsedown();
        return $parsedown->text($content);
    }

phpmailer

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;
    }

sass

public function sass($content, $params = [])
    public function sass($content, $params = [])
    {
        $this->require_php_lib('scss');
        $scss = new scssc();
        return $scss->compile($content);
    }

Methods starting with _ hooks and helpers; not routable as page actions

__call

public function __call($name, $args)

Catch missing method call.

    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args);
    }

__clone

public function __clone()

We cleanup object properties when cloning.

    public function __clone()
    {
        foreach ((array) get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
    }

__toString

public function __toString(): string

Need to avoid calling render() without params.

    public function __toString()
    {
        try {
            return (string) $this->render();
        } catch (Exception $e) {
            return '';
        }
    }

_before_clone

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;
    }

_notify

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;
    }

_vendor_dirs

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];
    }