PHP на фронтенде

PHP на фронтенде

Думаю, вы видели множество сайтов, использующих javascript на фронтенде, а как насчет использования PHP на стороне клиента? Но как, спросите вы? Поскольку PHP написан на C, то можно скомпилировать версию, которая может работать в браузера через WebAssembly.

Хорошая идея? Давайте разберемся!

Компиляция PHP в WebAssembly

Скомпилировать PHP для браузеров на самом деле не так сложно, как вы думаете. Используя скрипты компиляции Oraoto, довольно просто получить PHP wasm билд, который работает так же, как PHP на сервере.

Ну, в основном. Пока он немного ограничен, так как позволяет вам только выполнять код через eval и получать строки, напечатанные PHP. Тем не менее, можно создать довольно крутые вещи.

Делаем Laravel Collection Playground

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

Хорошим шансом проверить PHP в браузере и создать что-то классное, в стиле jsfiddle, оказалось написание работы с коллекциями.

Вы можете посмотреть репозиторий на Github или попробовать его вживую здесь.

PHP client side

Как это работает

Я создал небольшой PHP пакет, который получает JSON и коллекцию от Vue. Код компилируется в phar (исполняемый файл php) вместе с кодом компонента коллекции Laravel.

Он просто преобразует json в коллекцию, а затем использует eval для выполнения. Результат или любые ошибки (перехваченные исключения или Throwables) преобразуются обратно в json и печатаются. Вернувшись в javascript, мы перехватываем стандартный stdout PHP и отображаем результат выполнения пользователю.

Работает на удивление хорошо, и благодаря поддержке PWA вы даже можете использовать его без интернета.

Что если бы мы могли запустить Laravel на стороне клиента?!

Круто, мы создали что-то, что запускает php’шный код, но коллекции это просто. Вот если бы мы могли запустить целое веб-приложение, полностью сделанное на Laravel на стороне клиента? Это реально было бы бессерверное приложение (serveless).

Проект TodoMVC часто используется для тестирования фреймворков и я подумал, что это хороший кандидат для запуска.

Первое препятствие, с которым мы столкнулись, — отправка запросов в Laravel, ведь нет веб-сервера и мы не можем просто использовать веб-запрос.

На самом деле мы можем запустить фреймворк с имитируемым запросом стандарта PSR7, аналогично тому, как вы запускаете фреймворк в интеграционном тесте. Поскольку мы можем взаимодействовать с PHP только при выполнении кода, то я обернул цикл запроса в функцию:

function run(string $requestAsJson, string $requestId)
{
    $requestData = json_decode($requestAsJson, true);

    require __DIR__ . '/bootstrap/autoload.php';

    $app = require_once __DIR__ . '/bootstrap/app.php';

    $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

    $_SERVER['CONTENT_TYPE'] = 'application/json';

    $request = \Illuminate\Http\Request::create(
        $requestData['uri'],
        $requestData['method'],
        $requestData['params'] ?? [],
        $requestData['cookies'] ?? [],
        [],
        $_SERVER,
        $requestData['body'] ?? null
    );

    /** @var \Illuminate\Http\Response $response */
    $response = $kernel->handle($request);

    echo json_encode([
        'id' => $requestId,
        'status' => $response->getStatusCode(),
        'content' => $response->getContent(),
        'headers' => $response->headers->all(),
    ]);

    $kernel->terminate($request, $response);
}

Быстро тестируем в консоли, чтобы убедиться, что он работает. Cобираем Laravel и все его зависимости в phar (виртуальная файловая система emscipten плохо обрабатывает большое количество файлов) и компилим его в бинарник WebAssembly.

Так как мы указали Laravel использовать SQLite, то состояние приложения может сохраняться между (поддельными) запросами. Однако оно будет сбрасываться при каждой перезагрузке, поскольку хранилище временное.

Протестировав его в консоли браузера, мы можем добавить новый элемент todo и выполнить запрос к методу index:

function request(data) {
    const reqId = Math.random().toString(36).substring(7);
    const code = `$phar = 'phar://app.phar';require $phar . '/index.php';run('${JSON.stringify(data)}', '${reqId}');echo PHP_EOL;`;
    const ret = phpModule.ccall('pib_eval', 'number', ['string'], [code]);
}

// Add a todo item
request({
  uri: '/api',
  method: 'POST',
  body: JSON.stringify({
      title: 'do washing'
  })
})

// Get the todo list
request({
  uri: '/api',
  method: 'GET'
})

{
    "status": 200,
    "content": "[{\"id\":1,\"title\":\"do washing\",\"order\":null,\"completed\":false,\"created_at\":\"2019-10-06 07:25:29\",\"updated_at\":\"2019-10-06 07:25:29\",\"url\":\"\\\/1\"}]",
    "headers": {
        "cache-control": [
            "no-cache, private"
        ],
        "content-type": [
            "application\/json"
        ],
        "date": [
            "Sun, 06 Oct 2019 07:25:53 GMT"
        ],
        "x-ratelimit-limit": [
            "60"
        ],
        "x-ratelimit-remaining": [
            59
        ],
    }
}

Заключение

Несмотря на то, что забавно разбираться с PHP в браузере, но все это еще далеко от того, чтобы можно было использовать всерьез. Вот неполный список недостатков:

  • Примерно в 5 раз медленнее, чем PHP обычно;
  • Использует >1ГБ памяти при запуске, что приводит к тому, что устройства начального уровня вешаются;
  • Работает только на последних десктопных версиях Chrome, Firefox и Safari;
  • Сборка wasm занимает много времени и требуется при каждом изменении кода;
  • Вы не можете делать ничего такого, что не может сделать JavaScript. Это означает отсутствие веб-запросов, что ограничивает потенциал;
  • Это требует от вас загрузки всего билда PHP + код (если вы не кэшируете его на стороне клиента), что составляет около 4 МБ;
  • Иногда он просто отказывается работать, а отлаживать очень сложно. Много вы не увидите, кроме кода выхода PHP;

Очевидно, что сейчас это не подходит для реального использования, но, возможно, в будущем мы могли бы писать PHP код на фронтенде.

Автор: atymic
Перевод: Demiurge Ash