Как расширить возможности runtime KPHP

от автора

Всем доброго дня, уважаемые читатели. В данной статье вы узнаете как добавить новые функции в runtime KPHP.

Совсем вкратце расскажу о том, что такое KPHP и на примере какой задачи вы узнаете о расширении возможностей runtime KPHP.

О KPHP

KPHP — компилируемый PHP. Де-факто PHP, транслированный в C++. В свою очередь это увеличивает производительность исходного кода как минимум потому что скомпилировано в бинарный файл.

О нашей задаче

Задача, которую мы решим, заключается в следующем — реализовать две функции для парсинга строк и файлов в формате ENV. Исключительно для демонстрации всех этапов добавления новых функций в runtime.

Приступаем

Итак, перейдём к нашему плану:

  1. Подготовим всё необходимое

  2. Добавим новые функции

  3. Напишем тесты

  4. Проверим работоспособность

Подготовим всё необходимое

Я работаю под Ubuntu 20.04. И для начала нам нужно установить следующее (в том случае, если у вас их нет):

  • git

  • make, cmake

  • g++,

  • python3,

  • pip3

  • php7.4

После установки вышеупомянутых пакетов, необходимо установить vk`шные. А перед тем, надо добавить репозитории:

sudo apt-get update  sudo apt-get install -y --no-install-recommends apt-utils ca-certificates gnupg wget  sudo wget -qO - https://repo.vkpartner.ru/GPG-KEY.pub | sudo apt-key add -  echo "deb https://repo.vkpartner.ru/kphp-focal/ focal main" >> /etc/apt/sources.list

И уже затем установить пакеты:

sudo apt-get update  sudo apt install git cmake make g++ gperf python3-minimal python3-jsonschema \             curl-kphp-vk libuber-h3-dev kphp-timelib libfmt-dev libgtest-dev libgmock-dev libre2-dev libpcre3-dev \             libzstd-dev libyaml-cpp-dev libnghttp2-dev zlib1g-dev php7.4-dev libmysqlclient-dev libnuma-dev

Проделав это, переходим к сборке KPHP из исходников:

# Клонируем репозиторий git clone https://github.com/VKCOM/kphp.git  # Заходим в папку репозитория cd kphp  # Переключаемся на ветку git checkout -b 'pmswga/env_parsing'  # Создаём папку build mkdir build  # Заходим в папку build cd build  # Просим cmake по CMakeLists.txt сотоврить нам чудо cmake ..  # Также просим make сотворить чудо  make -j6 all

Сборка должна пройти успешно. Что мы получили в итоге? В корне репозитория мы получим новую папку objs и содержимое при ней:

kphp/ ├─ build/                      <-- Если вы ещё не вышли из этой папки, то вы тут :) ├─ objs/ │  ├─ bin/ │  │  ├─ kphp2cpp              <-- Наш kphp компилятор. Остальное нас не интересует :( │  │  ├─ tl2php │  │  ├─ tl-compiler │  ├─ flex/ │  │  ├─ libvk-flex-data.o │  │  ├─ libvk-flex-data.so │  ├─ generated/* │  ├─ vkext/ │  │  ├─ modules/ │  │  │  ├─ vkext.so │  │  ├─ modules7.4/ │  │  │  ├─ vkext.so

Вот мы и собрали последнюю версию KPHP из исходников. Приготовления завершены, теперь можем переходить к добавлению функций.

Добавим новые функции

Кратко обрисую алгоритм добавления новых функций, в такой схеме:

kphp/ ├─ builin-functions/_functions.txt  1) Добавить интерфейс функции сюда ├─ runtime/ │  ├─ *.h                           2) Добавить h-файлы с объявлением функций  │  ├─ *.cpp                         3) Добавить cpp-файлы с реализацией функций │  ├─ runtime.cmake                 4) Добавить имена cpp-файлов в переменную KPHP_RUNTIME_SOURCES

После чего можно смело запускать make и убедиться, что всё добавлено без ошибок и собирается.

Теперь на нашем конкретном примере:

  1. В файле _functions.txt добавим интерфейсы функций parse_env_file и parse_env_string. Обратите внимание, на то как указываются типы. В целом всё ясно. Принимают строки, возвращают массивы строк.

function parse_env_file($filename ::: string) ::: string[]; function parse_env_string($env_string ::: string) ::: string[];
  1. Добавляем parsing_functions.h со следующим содержимым:

#pragma once  #include "runtime/kphp_core.h" #include <regex> #include <fstream> #include <sstream>  /*  * Cool functions. А именно функции для очистки строк от ненужного  */  string clearSpecSymbols(const string &str);  string clearSpaces(const string &str);  string clearEOL(const string &str);  string clearQuotes(const string &str);  string clearString(const string &str);  string trim(const string &str);  /*  * The best funtions.   * А именно функции, которые проверяют строки по регулярным выражениям   * и функции, которые возвращают части одной ENV-записи  */  bool isEnvComment(const string &env_comment);  bool isEnvVar(const string &env_var);  bool isEnvVal(const string &env_val);  string get_env_var(const string &env_entry);  string get_env_val(const string &env_entry);  /*  * Env file|string parsing functions.   * А именно функции будут подставляться в сгенерированном коде  */  array<string> f$parse_env_file(const string &filename);  array<string> f$parse_env_string(const string &env_string);
  1. Добавляем в parsing_functions.cpp следующий код:

#include "parsing_functions.h"  string clearSpecSymbols(const string &str) {   return string(     std::regex_replace(str.c_str(), std::regex(R"([\t\r\b\v])"), "").c_str()   ); }  string clearSpaces(const string &str) {   return string(     std::regex_replace(str.c_str(), std::regex(" += +"), "=").c_str()   ); }  string clearEOL(const string &str) {   return string(     std::regex_replace(str.c_str(), std::regex("\\n"), " ").c_str()   ); }  string clearQuotes(const string &str) {   return string(     std::regex_replace(str.c_str(), std::regex("[\"\']"), "").c_str()   ); }  string clearString(const string &str) {   string clear_string = clearSpecSymbols(str);   clear_string = clearSpaces(clear_string);   clear_string = clearQuotes(clear_string);   clear_string = trim(clear_string);    return clear_string; }  string trim(const string &str) {   if (str.empty()) {     return {};   }    size_t s = 0;   size_t e = str.size()-1;    while (s != e && std::isspace(str[s])) {     s++;   }    while (e != s && std::isspace(str[e])) {     e--;   }    return str.substr(s, (e-s)+1); }  /* Example: #APP_NAME=Laravel */ bool isEnvComment(const string &env_comment) {   return std::regex_match(     env_comment.c_str(),      std::regex("^#.*", std::regex::ECMAScript)   ); }  /* Example: APP_NAME */ bool isEnvVar(const string &env_var) {   return std::regex_match(     env_var.c_str(),      std::regex("^[A-Z]+[A-Z\\W\\d_]*$", std::regex::ECMAScript)   ); }  /* Example: Laravel */ bool isEnvVal(const string &env_val) {   return std::regex_match(     env_val.c_str(),      std::regex("(.*\n(?=[A-Z])|.*$)", std::regex::ECMAScript)   ); }  /* Example: APP_NAME=Laravel -> APP_NAME */ string get_env_var(const string &env_entry) {   string::size_type pos = env_entry.find_first_of(string("="), 0);    if (pos == string::npos) {     return {};   }    return env_entry.substr(0, pos); }  /* Example: APP_NAME=Laravel -> Laravel */ string get_env_val(const string &env_entry) {   string::size_type pos = env_entry.find_first_of(string("="), 0);    if (pos == string::npos) {     return {};   }    pos++;      return env_entry.substr(pos, env_entry.size() - pos); }  /*  * Вот собственно реализация parse_env_file  */ array<string> f$parse_env_file(const string &filename) {   if (filename.empty()) {     return {};   }    std::ifstream ifs(filename.c_str());    if (!ifs.is_open()) {     php_warning("File not open");     return {};   }    array<string> res(array_size(1, 0, true));    std::string env_entry;   while (getline(ifs, env_entry)) {     string env_entry_copy = clearString(string(env_entry.c_str()));      if (!env_entry_copy.empty() && !isEnvComment(env_entry_copy)) {       string env_var = get_env_var(env_entry_copy);        if (env_var.empty()) {         php_warning("Invalid env string format %s", env_entry_copy.c_str());         return {};       }        string env_val = get_env_val(env_entry_copy);        if (isEnvVar(env_var) && isEnvVal(env_val)) {         res.set_value(env_var, env_val);       } else {         php_warning("Invalid env string format %s", env_entry_copy.c_str());         return {};       }     }   }    ifs.close();    return res; }  /*  * Вот собственно реализация parse_env_string  */ array<string> f$parse_env_string(const string &env_string) {   if (env_string.empty()) {     return {};   }    array<string> res(array_size(0, 0, true));    string env_string_copy = clearString(env_string);   env_string_copy = clearEOL(env_string_copy);    std::stringstream ss(env_string_copy.c_str());   std::string str;    while (getline(ss, str, ' ')) {     string env_entry = string(str.c_str());      if (!isEnvComment(env_entry)) {       string env_var = get_env_var(env_entry);        if (env_var.empty()) {         php_warning("Invalid env string format %s", env_entry.c_str());         return {};       }        string env_val = get_env_val(env_entry);        if (isEnvVar(env_var) && isEnvVal(env_val)) {         res.set_value(env_var, env_val);       } else {         php_warning("Invalid env string format %s", env_entry.c_str());         return {};       }     }   }    return res; }

Как видите, для того чтобы функции реально работали в runtime их нужно называть с префиксом f$ в начале. Ибо именно они будут подставляться в сгенерированном коде (позже сами увидите). В остальном, плодите кода столько, сколько хотите 🙂

Поговорим о двух важных вещах — это array<string> и string. Это реализация массивов и строк в самом runtime KPHP, а не std`шная (Сам бы Александр Степанов дал бы по рукам за такие методы как set_value и другие).

array<string> позволяет нам делать ассоциативные и обычные массивы.

string позволяет привести себя в int, float, bool, string.

  1. И последнее, добавляем в наш parsing_functions.cpp в cmake файл:

# тут ещё немного cmake   prepend(KPHP_RUNTIME_SOURCES ${BASE_DIR}/runtime/         ${KPHP_RUNTIME_DATETIME_SOURCES}         ${KPHP_RUNTIME_MEMORY_RESOURCE_SOURCES}         ${KPHP_RUNTIME_MSGPACK_SOURCES}         ${KPHP_RUNTIME_JOB_WORKERS_SOURCES}         ${KPHP_RUNTIME_SPL_SOURCES}         ${KPHP_RUNTIME_PDO_SOURCES}         ${KPHP_RUNTIME_PDO_MYSQL_SOURCES}         allocator.cpp         array_functions.cpp         bcmath.cpp         common_template_instantiations.cpp         confdata-functions.cpp         confdata-global-manager.cpp         confdata-keys.cpp         critical_section.cpp         curl.cpp         exception.cpp         files.cpp         from-json-processor.cpp         instance-cache.cpp         instance-copy-processor.cpp         inter-process-mutex.cpp         interface.cpp         json-functions.cpp         json-writer.cpp         kphp-backtrace.cpp         mail.cpp         math_functions.cpp         mbstring.cpp         memcache.cpp         memory_usage.cpp         migration_php8.cpp         misc.cpp         mixed.cpp         mysql.cpp         net_events.cpp         on_kphp_warning_callback.cpp         openssl.cpp         parsing_functions.cpp                            <-- Наш файл         php_assert.cpp         profiler.cpp         regexp.cpp         resumable.cpp         rpc.cpp         serialize-functions.cpp         storage.cpp         streams.cpp         string.cpp         string_buffer.cpp         string_cache.cpp         string_functions.cpp         tl/rpc_tl_query.cpp         tl/rpc_response.cpp         tl/rpc_server.cpp         typed_rpc.cpp         uber-h3.cpp         udp.cpp         url.cpp         vkext.cpp         vkext_stats.cpp         ffi.cpp         zlib.cpp         zstd.cpp)  # и тут ещё немного cmake 

Ура! Можно компилировать и проверять работоспособность.

Напишем тесты

Однако, никто же не поверит, что у вас всё работает, если вы не напишите для этого тесты… И никто всерьёз вас не воспримет, когда вы без тестов отправите pull-request. Поэтому приступим.

Что надо знать о тестах?

kphp/ ├─ tests/ │  ├─ cpp/                        <---- Здесь cpp тесты  │  │  ├─ compiler                 <---- Тесты компилятора │  │  ├─ runtime                  <---- Тесты runtime │  │  │   ├─ *.cpp                1) Добавляем cpp файлы тестов │  │  │   ├─ runtime-tests.cmake  2) Добавлем имена cpp файлов в переменную RUNTIME_TESTS_SOURCES │  │  ├─ server                   <---- Тесты сервера │  ├─ phpt/                       <---- Здесь php тесты │  │  ├─ my_folder_with_tests     3) Создаём свою папку для тестов |  |  |   ├─ 001_*.php            4) Создаём свои *.php тесты с нумерацией |  ├─ kphp_tester.py              5) Запустить ранее написанные тесты с помощью этого скрипта

CPP тесты написаны с помощью gtest и являются обычными unit-тестами.

Однако, php тесты работают следующим образом. Пишется код на php, в том числе с функциями, которые есть только kphp. Затем они запускаются с помощью kphp_tester.py и выполняются как обычный php код, так и kphp. Затем их результаты сравниваются и делается вывод, тест пройден или нет.

Вопрос вот в чём, откуда обычный php узнает о той же функции parse_env_string и parse_env_file, если их в принципе нет? Для этого нужны php-polyfills (в своём роде заглушки). Далее всё увидите сами.

Для запуска cpp тестов:

# Перейдём в папку build cd build  # Соберём всё ещё раз make -j6 all  # Запустим тесты ctest -j6

В результате все тесты должны выполниться успешно.

Для запуска php тестов нужно проделать следующее:

# Сначала скачать php-polyfiils git clone https://github.com/VKCOM/kphp-polyfills.git  # Зайдём в папку kphp-polyfiils cd kphp-polyfills  # Установим пакеты и сгенерируем autoload  composer install  # Зададим переменную окружения KPHP_TESTS_POLYFIILS_REPO  export KPHP_TESTS_POLYFILLS_REPO=$(pwd)

Такая многоходовочка даёт нам следующее, что при запуске php тестов, они будут обращаться по этому пути подтягивать «заглушки».

И вот теперь, действительно, для запуска php тестов:

# Запуск всех тестов tests/kphp_tester.py  # Запуск конкретного теста tests/kphp_tester.py 001_*.php

Вуаля!

CPP тесты

Теперь к нашим барашкам (к f$parse_env_string и f$parse_env_file). Добавим parsing-functions-tests.cpp со следующим кодом:

#include <gtest/gtest.h> #include "runtime/parsing_functions.h"  TEST(parsing_functions_test, test_isEnvComment) {   ASSERT_FALSE(isEnvComment(string("")));   ASSERT_FALSE(isEnvComment(string("APP_NAME=Laravel")));   ASSERT_TRUE(isEnvComment(string("#APP_NAME=Laravel"))); }  TEST(parsing_functions_test, test_isEnvVar) {   ASSERT_FALSE(isEnvVar(string("")));   ASSERT_FALSE(isEnvVar(string("!APP_NAME")));   ASSERT_TRUE(isEnvVar(string("APP_NAME"))); }  TEST(parsing_functions_test, test_isEnvVal) {   ASSERT_TRUE(isEnvVal(string("")));   ASSERT_TRUE(isEnvVal(string("true")));   ASSERT_TRUE(isEnvVal(string("local")));   ASSERT_TRUE(isEnvVal(string("80")));   ASSERT_TRUE(isEnvVal(string("127.0.0.1")));   ASSERT_TRUE(isEnvVal(string("https://localhost")));   ASSERT_TRUE(isEnvVal(string("\'This is my env val\'")));   ASSERT_TRUE(isEnvVal(string("\"This is my env val\""))); }  TEST(parsing_functions_test, test_get_env_var) {   string str("APP_NAME=Laravel");   string env_var = get_env_var(str);    ASSERT_STREQ(string("").c_str(), get_env_var(string("")).c_str());   ASSERT_STREQ("APP_NAME", env_var.c_str());   ASSERT_EQ(strlen("APP_NAME"), env_var.size()); }  TEST(parsing_functions_test, test_get_env_val) {   string str("APP_NAME=Laravel");   string env_val = get_env_val(str);    ASSERT_STREQ("Laravel", env_val.c_str());   ASSERT_EQ(string("Laravel").size(), env_val.size()); }  /*  * Тестируем функцию parse_env_string  */ TEST(parsing_functions_test, test_parse_env_string) {   string env_string = string(R"(APP_NAME=Laravel APP_ENV=local #APP_KEY=base64:mtlb8hldh5hZ0GlLzbhInsV531MSylspRI4JsmwVal8= APP_DEBUG=true T1="my" T2='my')");    array<string> res(array_size(0, 0, true));    res = f$parse_env_string(env_string);   ASSERT_EQ(res.size().string_size, 5);    ASSERT_TRUE(res.has_key(string("APP_NAME")));   ASSERT_STREQ(res.get_value(string("APP_NAME")).c_str(), string("Laravel").c_str());   ASSERT_TRUE(res.has_key(string("APP_ENV")));   ASSERT_STREQ(res.get_value(string("APP_ENV")).c_str(), string("local").c_str());   ASSERT_TRUE(res.has_key(string("APP_DEBUG")));   ASSERT_STREQ(res.get_value(string("APP_DEBUG")).c_str(), string("true").c_str());   ASSERT_TRUE(res.has_key(string("T1")));   ASSERT_STREQ(res.get_value(string("T1")).c_str(), string("my").c_str());   ASSERT_TRUE(res.has_key(string("T2")));   ASSERT_STREQ(res.get_value(string("T2")).c_str(), string("my").c_str()); }  /*  * Тестируем функцию parse_env_file  */ TEST(parsing_functions_test, test_parse_env_file) {   std::ofstream of(".env.example");    if (of.is_open()) {     of << "APP_NAME=Laravel "<< std::endl;     of << "APP_ENV=local" << std::endl;     of << "APP_DEBUG=true" << std::endl;     of.close();   }    array<string> res(array_size(0, 0, true));    res = f$parse_env_file(string("file not found"));   ASSERT_EQ(res.size().string_size, 0);    res = f$parse_env_file(string(".env.example"));   ASSERT_TRUE(res.has_key(string("APP_NAME")));   ASSERT_STREQ(res.get_value(string("APP_NAME")).c_str(), string("Laravel").c_str());   ASSERT_TRUE(res.has_key(string("APP_ENV")));   ASSERT_STREQ(res.get_value(string("APP_ENV")).c_str(), string("local").c_str());   ASSERT_TRUE(res.has_key(string("APP_DEBUG")));   ASSERT_STREQ(res.get_value(string("APP_DEBUG")).c_str(), string("true").c_str()); } 

Теперь их можно запустить и убедиться, что они успешно выполняются.

Результаты cpp тестов
Результаты cpp тестов

PHP тесты

Вспоминаем про php-polyfills, идём в соседний репозиторий, в корне которого находим файл kphp_polyfiils.php добавляем в него следующий код:

#ifndef KPHP  # тут много какого-то кода  #region env parsing  /**  * parse_env_string return associative array by parsed string  *   */ function parse_env_string(string $env_string) {   if (empty($env_string)) {     return [];   }    $get_env_entry = function ($env_string) {     $env_entry = explode('=', $env_string, 2);      if (count($env_entry) !== 2) {       die("parse error\n");     }      return [       'env_var' => trim($env_entry[0]),       'env_val' => trim($env_entry[1])     ];   };    $lines = explode(' ', $env_string);   $env = [];     foreach ($lines as $line) {     $env_entry = $get_env_entry($line);      $env[trim($env_entry['env_var'])] = trim($env_entry['env_val']);    }    return $env; }  /**  * parse_env_string return associative array by parsed file  *   */ function parse_env_file(string $filename) {    if (empty($filename)) {     return [];   }    if (!is_file($filename)) {     return [];   }    if (!file_exists($filename)) {     return [];   }    $env_string = file_get_contents($filename);    return parse_env_string($env_string); }  #endregion  #endif

По существу мы реализовали парсинга env строк и файлов в формате ENV. Что собственно и можно было сделать изначально, создав даже целую либу (kenv).

Теперь же создадим по пути tests/phpt/parsing/001_parsing_env.php и добавим в него следующий код.

@ok                                              # <-- Тег обозначает должен ли этот код компилироваться на KPHP <?php  require_once 'kphp_tester_include.php';          # <-- Подключаем php-polyfills  function test_parse_env_string_empty() {         # <-- Сами "тесты"     var_dump(parse_env_string('')); }  function test_parse_env_string_one() {     var_dump(parse_env_string('APP_NAME=Laravel')); }  function test_parse_env_string_many() {     var_dump(parse_env_string('APP_NAME=Laravel APP_DEBUG=true APP_ENV=local')); }  function test_parse_env_file_empty() {     var_dump(parse_env_file('')); }  function test_parse_env_file_not_found_empty() {     var_dump(parse_env_file('file not found')); }  function test_parse_env_file_one() {     $filename = tempnam("", "wt");     $fp = fopen($filename, "a");     fwrite($fp, "APP_NAME=Laravel");     fclose($fp);      var_dump(parse_env_file($filename)); }  function test_parse_env_file_many() {     $filename = tempnam("", "wt");     $fp = fopen($filename, "a");     fwrite($fp, "APP_NAME=Laravel");     fwrite($fp, "APP_DEBUG=true");     fwrite($fp, "APP_ENV=local");     fclose($fp);      var_dump(parse_env_file($filename)); }  test_parse_env_string_empty();                   # <-- Вызов функций test_parse_env_string_one(); test_parse_env_string_many();  test_parse_env_file_empty(); test_parse_env_file_not_found_empty(); test_parse_env_file_one(); test_parse_env_file_many();

Запустим написанный тест:

tests/kphp_tester.py 001_parse_env

И вот, заветное слово passed.

Результаты php тестов
Результаты php тестов

Проверяем работоспособность

Итого, вот какие изменения мы внесли, чтобы реализовать функции parse_env_file и parse_env_string.

# Репозиторий kphp kphp/ ├─ builin-functions/_functions.txt       <-- Добавили интерфейсы parse_env_file и parse_env_string ├─ runtime/ │  ├─ parsing_functions.h                <-- Добавили объявление функций  │  ├─ parsing_functions.cpp              <-- Добавили реализацию функций │  ├─ runtime.cmake                      <-- Добавили parsing_functions.cpp в переменную KPHP_RUNTIME_SOURCES ├─ tests/ │  ├─ cpp/ │  │  ├─ runtime │  │  │   ├─ parsing-functions-tests.cpp <-- Добавили cpp тесты │  │  │   ├─ runtime-tests.cmake         <-- Добавили parsing-functions-tests.cpp в переменную RUNTIME_TESTS_SOURCES │  ├─ phpt/ │  │  ├─ parsing                         <-- Создали папку parsing |  |  |   ├─ 001_parse_env.php           <-- Добавили php тесты  # Репозиторий kphp-polyfills kphp-polyfills/ ├─ kphp_polyfills.php                    <-- Добавили php`шные реализации parse_env_file и parse_env_string

Теперь можем посмотреть на наши плоды. Создадим index.php и напишем следующий код:

<?php     $env_string = "APP_NAME=Laravel APP_DEBUG=true APP_ENV=local";      $res = parse_env_string($env_string);      print_r('<pre>');     print_r($res);     print_r('</pre>');      $res = parse_env_file('.env.example');      print_r('<pre>');     print_r($res);     print_r('</pre>');

Скомпилируем его:

./kphp2cpp index.php

Запустим и получим следующее:

./kphp_out/server -H 8000 -f 2                    ^       ^                    |       |                    |     Поднимаем двух рабочих работу работать                    |                  Поднимаем localhost:8000
Результат работы скомпилированного index.php
Результат работы скомпилированного index.php

А вот что сгенерировано на С++:

//crc64:912a10e8beed9098 //crc64_with_comments:bc9f187534f26ce2 #include "runtime-headers.h" #include "o_6/src_indexbccbb8a09559268e.h" extern string v$env_string;  extern array< string > v$res;  extern bool v$src_indexbccbb8a09559268e$called;  extern string v$const_string$us3e8066aa5eeccc54;  extern string v$const_string$us531c70314bd2d991;  extern string v$const_string$usd04f12c090cf2e22;  extern string v$const_string$use301963cf43e4d3a;  //source = [index.php] //3:     $env_string = "APP_NAME=Laravel APP_DEBUG=true APP_ENV=local"; Optional < bool > f$src_indexbccbb8a09559268e() noexcept  {   v$src_indexbccbb8a09559268e$called = true;   v$env_string = v$const_string$us3e8066aa5eeccc54; //4:  //5:     $res = parse_env_string($env_string);   v$res = f$parse_env_string(v$env_string);                       <-- Вот вызов нашей функции  //6:  //7: print_r('<pre>');   f$print_r(v$const_string$usd04f12c090cf2e22); //8:     print_r($res);   f$print_r(v$res); //9: print_r('</pre>');   f$print_r(v$const_string$us531c70314bd2d991); //10:  //11:  //12:     $res = parse_env_file('.env.example');   v$res = f$parse_env_file(v$const_string$use301963cf43e4d3a);    <-- И вот вызов нашей функции   //13:  //14: print_r('<pre>');   f$print_r(v$const_string$usd04f12c090cf2e22); //15:     print_r($res);   f$print_r(v$res); //16:    f$print_r(v$const_string$us531c70314bd2d991);   return Optional<bool>{}; }

Заключение

Спасибо всем кто дочитал до конца. Надеюсь что поставленная цель выполнена.

Если вы хотите присоединиться к сообществу KPHP, то добро пожаловать в чат.

По изложенной теме:

Дополнительные ссылки:


ссылка на оригинал статьи https://habr.com/ru/post/701216/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *