Эпизод 1. Механизмы

Здравствуйте всем, это первая серия сериала “ООП здорового человека”. Есть 3 механизма, без которых ООП не ООП – это наследование, инкапсуляция и полиморфизм. Также, есть механизм “сокрытие”, который приписываю к части инкапсуляции.

Наследование 

Наследование — концепция объектно-ориентированного программирования, согласно которой абстрактный тип данных может наследовать данные и функциональность некоторого существующего типа, способствуя повторному использованию компонентов программного обеспечения. Эт-то о че нам говорит статья Википедии… я же предлагаю пока не рассматривать абстракцию(поскольку это не необходимое условие для данного механизма), а рассмотреть само явление наследования. Как и в жизни, наследник получает в свое распоряжение активы наследуемого; на примере класса Person из этой статьи:

<?php
class Person
{
    public int $id;
    public string $name;
    public string $secondName;
    public string $sex;
    public string $birthday;

    public function toArray()
    {
        return get_object_vars($this);
    }
}

допустим персона это частный случай каких-то единиц(персона наследует элемент)

<?php
class Item
{
    public int $id;
    public string $name;

    public function toArray()
    {
        return get_object_vars($this);
    }
}

class Person extends Item
{
    public string $secondName;
    public string $sex;
    public string $birthday;
}

При этом класс Person включает все методы и свойства класса Item

<?php
// классы ранее

print_r(get_class_methods('Person'));
print_r(get_class_vars('Person'));

// вывод
Array
(
    [0] => toArray
)
Array
(
    [secondName] => 
    [sex] => 
    [birthday] => 
    [id] => 
    [name] => 
)

Однако, их(активы наследуемого класса) можно скрыть от наследника – об этом в следующей статье. Смысл наследования состоит в минимальном копи-пасте кода. В некоторых языках наследоваться можно от многих, в других же – от одного класса(пример PHP – там множественное наследование спародировано трейтами – об этом в следующих сериях).

Инкапсуляция

Инкапсуляция (англ. encapsulation, от лат. in capsula) — размещение в одном компоненте данных и методов, которые с ними работают. В реализации большинства языков программирования обеспечивает механизм сокрытия, позволяющий разграничивать доступ к различным компонентам программы. Этот принцип в классе Person сохранен, т.к. все методы класса работают с данными, лежащими в нем же.

Инкапсуляция считается неполной если не реализовано сокрытие. Сокрытие – запрет на прямой доступ к полям объекта, вместо этого – доступ через модификаторы. На примере класса Item это выглядит так

<?php
class Item
{
    protected int $id;
    protected string $name;

    public function setId($id)
    {
        $this->id = (int) $id;
    }

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        $this->name = strval($name);
    }

    public function getName()
    {
        return $this->name;
    }

    public function toArray()
    {
        return get_object_vars($this);
    }
}

Для сокрытия модификатор (свойства) public заменяется на protected/private(о разнице также в следующих сериях) и доступ открывается через “сеттеры” и “геттеры”(собственно, модификаторы).

Полиморфизм

И снова Википедия: “Полиморфизм в языках программирования и теории типов — способность функции обрабатывать данные разных типов”. Если мы рассмотрим класс Item(вариант с сокрытием), то метод setId(setName также) имеет в себе реализацию полиморфизма, т.к. принимает в себя параметр различных типов и приводит его к нужному.

Основные парадигмы программирования

После изучения языка программирования, его синтаксиса, приходит вопрос: какой парадигмы придерживаться? На самом деле нет(в большинстве случаев). Парадигма программирования это архитектура кода, следование одной из них необходимо, чтоб другой разработчик мог быстрее понять как ваша прога работает. Почему-же этот выбор, за частую, сделан за вас? все просто: если это не одноразовый скриптик, а часть какой-то программы, писать его нужно будет в том же архитектурном стиле, что и предыдущий код(нынче программы редко пишутся с нуля). Итак, рассмотрим 3 парадигмы: процедурное, функциональное и объектно-ориентированное программирование.

И еще, для того чтоб было более наглядно – приведу код. Код будет выполнять недавнее мое тестовое задание – вот и само задание:

<?php
/*
Implement Mankind class, which works with Person instances.

General requirements:
- there can only exist a single instance of the class (Martians are not mankind...)
- allow to use the instance as array (use person IDs as array keys) and allow to loop through the instance via foreach

Required operations:
- Load people from the file (see below)
- Get the Person based on ID
- get the percentage of Men in Mankind



Loading people from the file:

Input file is in CSV format. Each person is in separate line. 
Each line contains ID of the person, name, surname, sex (M/F) and birth date in format dd.mm.yyyy.
Attributes are separated by semicolon (;) File is using UTF8 encoding. 
 
Example:
123;Michal;Walker;M;01.11.1962
3457;Pavla;Nowak;F;13.04.1887
*/

Для тех, кто слаб в английский(таких, как я) – простыми словами:

Создать класс Mankind, работающий с классом Person и делающий следующее:

  • читает xml-строку указанного формата и извлекает из нее данные о персонах;
  • уметь выдавать данные по отдельной персоне;
  • рассчитать процентное содержание мужчин среди населения;
  • представить население в виде массива персон.

там еще есть требования к классам, но их опущу(в рамках этой статьи).

Процедурное программирование

Определение с Википедии “Процедурное программирование — программирование на императивном языке, при котором последовательно выполняемые операторы можно собрать в подпрограммы, то есть более крупные целостные единицы кода, с помощью механизмов самого языка”. К определению добавить нечего…

Теперь к сути, а суть такая – это просто текст кода(то, как программирует большинство при изучении ЯП). Функционал моего тестового задания, в процедурном стиле будет выглядеть так:

<?php
$persons = [];
$maleCount = 0;
// открытие файла для чтения
$resource = fopen($filePath, 'r');;
// построчное чтение
while ($line = fgets($resource)) {
    // разбиение строки в массив
    $personData = str_getcsv($line, ';');
    // формирование массива персон
    $persons[$personData[0]] = [
        'id' => $personData[0],
        'name' => $personData[1],
        'second_name' => $personData[2],
        'sex' => $personData[3],
        'birthday' => $personData[4]
    ];
    // считаем мужчинн
    if ($personData[3] === 'M') {
        $maleCount++;
    }
}
// получаем персону
$person123 = $persons[123];
// получим % мужчин
$genderStats = $maleCount * 100 / count($persons);

Функциональный стиль программирования

Все та же википедия(в иных источниках определение такое же): “Функциональное программирование – парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних”. Для большего понимания, моими словами(ну может так все его объясняют) – программирование, при котором результат работы первой функции является входным параметром следующей.

Код моей программки, переписанный в этом стиле, будет таким

<?php
$persons = [];
$maleCount = 0;
// открытие файла для чтения
$resource = fopen($filePath, 'r');;
// чтение файла в массив строк
$lines = file($filePath);
// обработка каждой строкм ф-ей parser
$persons = array_map('parser', $lines);
// получаем персону
$person123 = getPeron(123, $persons);
// получим % мужчин
$maleStats = getMaleStats($persons);

function parser($line)
{
    $personData = str_getcsv($line, ';');

    return [
        'id' => $personData[0],
        'name' => $personData[1],
        'second_name' => $personData[2],
        'sex' => $personData[3],
        'birthday' => $personData[4]
    ];
}

function getPeron($id, $persons)
{
    $index = array_search(['id' => $id], $persons);

    return $persons[$index] ?? "Персоны с этим  идентификатором[{$index}] не существует";
}

function getMaleStats($persons)
{
    $count = 0;

    foreach ($persons as $person) {
        if ($person['sex'] === 'M') {
            $count++;
        }
    }

    return $count * 100 / count($persons);
}

преимущества такого кода – появляется возможность пере использования кода, что приводит к возможности масштабирования приложения. Недостатком же есть увеличение времени работы скрипта(незначительное)…

Объектно-ориентированное программирование

Традиционно, начнем из определения в Вики: “Объектно-ориентированное программирование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования”. И добавить нечего… даже своими словами – все предельно четко и ясно(как по мне).

Перепишем код порги в этом стиле. сразу скажу, это ООП “курящего человека”, почему так – объясню в следующих статьях.

<?php
// инициализация объекта
$mankindObj = new Mankind($filePath);
// поллучение массива персон
$persons = $mankindObj->toArray();
// получаем персону
$person123 = $mankindObj->getPersonById(123);
// получим % мужчин
$maleStats = $mankindObj->getMaleStats();

class Person
{
    public int $id;
    public string $name;
    public string $secondName;
    public string $sex;
    public string $birthday;

    public function toArray()
    {
        return get_object_vars($this);
    }
}

class Mankind
{
    public array $mankind;

    public function __construct($path)
    {
        $lines = file($path);

        $this->load($lines);
    }

    public function getPersonById($id)
    {
        return $this->mankind[$id] ?? "Персоны с этим  идентификатором[{$id}] не существует";
    }

    public function getMaleStats()
    {
        $count = 0;

        foreach ($this->mankind as $person) {
            if ($person->sex === 'M') {
                $count++;
            }
        }

        return $count * 100 / count($this->mankind);
    }

    public function toArray()
    {
        $mankind = [];

        foreach ($this->mankind as $person) {
            $mankind[$person->id] = $person->toArray();
        }

        return $mankind;
    }

    protected function load($lines)
    {
        array_map([$this, 'addPerson'], $lines);
    }

    protected function addPerson($line)
    {
        $person = new Person();

        $personData = str_getcsv($line, ';');
        $person->id = (int)$personData[0];
        $person->name = $personData[1];
        $person->secondName = $personData[2];
        $person->sex = $personData[3];
        $person->birthday = $personData[4];

        $this->mankind[(int)$personData[0]] = $person;
    }
}

В этом стиле, несмотря на то, что код максимально длинный, все максимально понятно. Это помимо того что дает возможность масштабировать систему – так еще и делает этот процесс более понятным(при адекватном наименовании классов, методов и т.д.) и быстрым(относительно). Преимущества:

  • Дополнительные пути для уникального нейминга(неймспейс, имя класса, имя метода/свойства);
  • Удобное и, в принципе, возможное масштабирование.

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

Имхо

Говорят, эта тема холиварна в кругах программистов, но я, как экстра-интроверт, не в курсе и потому выскажусь. После изучения синтаксиса ЯП, переменных и др. его основ, изучите ООП(до изучения фреймворков, зная объектное вы быстрее поймете любой из них). Без него не выйдет написать ни один крупный проект да и найти работу программистом. К примеру, больше половины тех.собеседований начаты были вопросами об ООП; каждое тех.собеседование содержало о нем вопросы; единственный раз, когда меня не позвали на тех.собеседование — когда я тестовое задание выполнил в процедурно-функциональном стиле(тогда мне показалось что ООП для него излишек).

WP REST API — параметры для фильтрации запросов.

Это продолжение статьи “WP REST API(дефолтное) — взаимодействие с WP сайтом,” в которой я повествовал о работе с дефолтным апи вордпрес. Однако, той статье я ничего не рассказываю о пагинации или получении связанных обьектов в одном запросе, ну и тд… в общем, эта статья ксть дополнением к той(по этому прочтите ее).

Параметры пагинации

Предположим, у нас появилась потребность получить обьекты постов запросом к WP REST API. В случаи если ЧПУ на сайте включены, запрос будет выглядеть так

https://guten.website/wp-json/wp/v2/posts

Cейчас мы получим JSON-строку, содержащую массив обьектов всех постов(или 100, если их больше). Но если нам не нужны они все? Здесь все просто – нужно указать параметры пагинации(аналогичные им же в обьекте WP_Query). Этот запрос ограничит нашу выборку 5-ю постами ч 2-ой страницы

https://guten.website/wp-json/wp/v2/posts?per_page=5&page=2

Как видно, были добавлены 2 GET-параметра – per_page и page. На деле их 4(5)(если порядок сортировки результатов относить к пагинации).

Третий параметр(offset) позволяет настроить сдвиг, т.е. количество постов, которое нужно пропустить(еще не придумал ситуации где он нужен, может, для разбития самой страницы…). И четвертый(order и orderby, да, их два – обманул) – определяют поле для сортировки(второй) и порядок сортировки(первый).

В итоге, запрос, который будет выводить последние 3 поста с второй страницы, содержащей по 5 постов на страницу и в порядке убывание id, выглядит так

https://guten.website/wp-json/wp/v2/posts?per_page=3&page=2&offset=2&sortby=id&sort=desc

Сортируемые поля у каждого типа обьекта разные… и, к полной картине, заголовки ответа(в полях X-WP-Total и X-WP-TotalPages) содержится

Заголовки HTTP-ответа
Заголовки HTTP-ответа

информация о количестве записей и количестве страниц, при данном разбиении. Немного уточнений по offset – он отбрасывает не n первых постов на каждой странице, а n первых постов согласно параметров сортировки.

Ссылание и встраивание

Каждый полученный обьект содержит поле _links, представляющее собой массив ссылок на апи-пути связанных обьектов. Также, эти массивы имеют поле embeddable, несущее информацию о том, можно ли этот связанный обьект встроить в текущий.

Для примера, воспользуемся обращением к постам; ответ содержит только id изображения миниатюры, но не саму ссылку на него(которую можно вставить в тег img, к примеру). В поле _links есть следующий обьект

Ссылка на связанный обьект
Ссылка на связанный обьект

В этом участке информации меня интересует не так ссылка на обьект, как то, что поле embeddable имеет значение true, т.е. обьект может быть встроен в текущий. Чтобы его встроить, нужно к текущему запросу дописатьь еще один гет-параметр _embed с значением нужного обьекта; в моем случаи

https://guten.website/wp-json/wp/v2/posts?per_page=5&page=2&_embed=wp:featuredmedia

при этом в каждом родительском объекте появится поле _embedded, содержащий требуемый обьект(равный тоому, который будет ответом по ссылке).

Встроенный объект
Встроенный объект

Фильтр полей

В WP REST API есть get-атрибут _fields, позволяющий заказать поля, которые нужно получить. Это нужно, как вы понимаете, для оптимизации запросов к серверу, что б он не “лег” в пиковые моменты… но не только! Также, этим параметром можно заказывать различные зарегистрированные метаполя. Рассмотрим пример получения id, title и метаполя counter; для начала зарегистрируем его в rest

<?php
// как мета данные
register_meta( 'post', 'counter', array(
	'type'              => 'string',
	'description'       => "Счетчик просмотров",
	'single'            => true,
	'sanitize_callback' => null,
	'auth_callback'     => null,
	'show_in_rest'      => true,
) );
// как обычное поле
add_action( 'rest_api_init', function(){

	register_rest_field( 'post', 'counter', array(
		'get_callback' => function( $post ){
			return get_post_meta( $post->ID, 'counter', true );
		},
		'update_callback' => null,
		'schema' => [
			'description' => "Счетчик просмотров", 
			'type' => 'string'
		],
	) );

} );

теперь можно получить коллекцию обьектов

https://guten.website/wp-json/wp/v2/posts?_fields=id,title,meta.counter
# или
https://guten.website/wp-json/wp/v2/posts?_fields=id,title,counter

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

Бесконечная страница: динамическая загрузка контента на WordPress

Все чаще и чаще на в сайты интегрируют элементы SPA, в частности “бесконечную страницу”… здравствуй, читатель! По ходу этой статьи будет описан принцип работы такой страницы, реализация и посадка на вордпресс.

SPA и бесконечная страница

В принципе, и SPA(Single Page Application – одно-страничное приложение) и бесконечная страница исповедуют один и тот же принцип(извиняюсь за тавтологию) – динамическая загрузка контента. Все очень просто – когда поступает событие(клик либо скролл, либо еще что-то), javascript’ом загружается новая порция контента и вставляется в страницу.

Реализация

Использовать буду jQuery(тут часть любителей хайповых React, Vue и прочего, вышли…). Обьясню свой выбор – зачем перегружать страницу яваскриптом? да, jQuery тоже библиотека без которой можно обойтись, но если он используется для многих других элементов – почему бы и нет?

Вот примерная разметка, составленная мной

<div class="container">
    <div class="page-1">
        <div class="card">
            <img src="path/to/img-1" class="img-fluid">
            <a href="path/to/single-post-1">
                <h2>Post Title 1</h2>
            </a>
            <p>post 1 excerpt</p>
        </div>
        ***
    </div>
    <div class="page-2"></div>
    ***
    <template id="post">
        <div class="card">
            <img src="" alt="" class="img-fluid">
            <a href="">
                <h2></h2>
            </a>
        </div>
    </template>
</div>

Немного о ней: поскольку я черпать контент буду в ворпрессе – то и использую пагинацию для разбивки на загружаемые блоки. Первую страницу загрузим обычным способом(каждая страница – это div class="page-%n"). В теге template будет содержаться разметка нашего поста – после загрузки пачки постов я буду ее клонировать.

Далее, нужно прибегнуть к javascript. Как я говорил ранее, на странице будет jQuery, поэтому сделаем AJAX-запрос к WP REST API со списком постов и их данными. Но прежде – определим условия для начала загрузки постов. Раз уж бесконечное полотно – значит условие это достижение нижней границы текущей страницы

var prevScroll = 0,
    page = 1,
    windowH = $(window).height(),
    positionY = $('.page-1').offset().top,
    blockH = $('.page-1').height(),
    template = $('#post').html(),
    currentScroll, tmpl

$(window).scroll(function (event) {
    currentScroll = $(this).scrollTop()

    if (currentScroll > prevScroll) {
        prevScroll = currentScroll

        if (Math.round(blockH + positionY) === (windowH + currentScroll)) {
            // Получаем посты, рисуем их и высчитываем новую границу для загрузки
        }
    }
})

Немного пояснений для этого куска кода. Эта строка(12) позволяет отсеять скролл вверх; можно было б отслеживать прокрутку колеса на мишке – но тогда скрипт был бы мертв на мобильниках. По поводу Math.round() свойства – высота(как и ширина) может быть дробной, в то время как значение скролла всегда целое число. Еще замечу – этот скрипт рабочий только в случаи если под блоком(не по разметке, а по позиции) с постами нет больше блоков(отсутствует футер). Дело в том, что при быстрой прокрутке scrollTop() имеет разрывы и тогда может произойти перескок нужного значения – потому, заменим его(===) простым сравниванием(>). Также, поскольку загрузка постов требует времени, используем коэффициент(что пользователю не пришлось ждать и он не покинул из-за этого сайт). В итоге, строка 15 трансформируется в

if (0.8 * (blockH + positionY) < (windowH + currentScroll)) {

Теперь, перейдем к тому, что внутри блока – содержимому строки 15. Само собой там загрузка и оттрисовка постов, но не только… вот полный код

$(window).off('scroll')
page++

$.get(
    '/wp-json/wp/v2/posts',
    {
        per_page: 5,
        page: page,
        _embed: 'wp:featuredmedia'
    },

    function (response) {
        $(response).each(function (i, e) {
            tmpl = $(template).clone()
            $(tmpl).find('img').attr('src', e._embedded["wp:featuredmedia"][0].source_url)
            $(tmpl).find('a').attr('href', e.link)
            $(tmpl).find('h2').text(e.title.rendered)
            $(tmpl).append(e.excerpt.rendered)

            $('.page-' + page).append(tmpl)
        })

        $('.page-' + page).find('img').on('load', function (i, e) {
            positionY = $('.page-' + page).offset().top
            blockH = $('.page-' + page).height()
        })

        if (page < pagesCount) {
            $(window).on('scroll', onScroll)
        }
    }
)

Традиционно, пояснения:

  • строка 1 “убивает” прослушивание события прокрутки. Это нужно потому, что js асинхронен и способен отослать несколько одинаковых запросов к серверу – что, в свою очередь, приведет к повторению постов(помимо ненужной нагрузки на сервер);
  • строки 4 – 33 сам запрос к серверу: первый аргумент это адрес эндпоинта WP REST API, который возвращает список постов, второй – обьект с параметрами постов, третий – функция, которая будет исполнена при получении ответа;
  • строки 23 – 26 – почему измерения высоты блока заперты в load’е? – у меня нет контейнера с размерами для картинки, а поскольку они грузятся отдельно от аякса – они изменяют размер блока постов через некоторое время;
  • строка 29 возвращаем прослушку прокрутки.

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

В общем, полный код бесконечной страницы(уже посаженой на шаблон страницы вордпрес) можно увидеть и взять здесь.

OpenCart 3. Написание модификаторов

OpenCart – cms, написанная на PHP и реализующая MVC шаблон проектирования. Специализирован данный движок под создание интернет-магазинов. С коробки вы получаете голый интернет-магазин: набор опций для продажи товаров без всего того, в чем нуждается современный сайт – социальные ссылки, метрика и т.д. Но все это исправимо по средству большого количества модулей…

Немного о модулях

OpenCart – это расширяемый модулями движок для сайтов. Каждый модуль это набот файлов котроллеров, моделей, шаблонов и локализации. Также, в состав модуля входят модификаторы – install.php, install.sql и modificator-name.ocmod.xml(два первых упразднены в версии 3)ю Файловая структура модуля выглядит следующим образом

module-name.ocmod.zip
    upload
        файлы модуля
    modificator-name.ocmod.xml(необязательный)

Я не причастен к разработке этого движка(во всяком случаи – пока что), но думаю, что install.php и install.sql упразднили по причине того, что их функционал с успехом можно запихнуть в метод install() контроллера модуля – который есть”обязательной программой” для любого модуля.

Модификатор это?

Как я уже сказал – OpenCart расширяемая модулями система, однако модули(чаще всего) требуют внедрения кастомного кода в существующие файлы. С этой целью была разработана система модификации VQMod, которая эволюционировала с второйверсии в OCMod. В целом, смысл таков: при применении файла-модификатора происходит чтение указанного файла в строку, применение модификаций и сохранение этой строки, как нового файла(по пути system/modification).

Файл модификатора представляет собой набор заголовков и инструкций для системы, которые я предлагаю рассмотреть.

Правила написания инструкций

Прежде всего – модификатор это XML файл, а все подобные файлы начинаются с представления

<?xml version="1.0" encoding="utf-8"?>

далее указываем системе что это именно модификатор – ставим парный тег modification

<modification>
    
</modification>

Первое, что здесь нужно указать это информация об авторе модификации и ее идентификация в системе

<name>Название модификатора</name>
<version>1.0</version>
<code>ID модификатора</code>
<author>Имя автора модификатора</author>
<link>ссылка на сайт автора</link>

После этой, “представительской” информации, следуют сами инструкции по тому, какой файл читать, что искать и что с этим делать.

В общем – указатель на файл – парный тег file

<file path="путь к файлу">
    
</file>

понятно, что атрибут path содержит путь к модифицируемому файлу. Следующий шаг – открытие парного тега operation. Можно было б решить, что это бессмысленный тег, однако – каждая замена в пределах одного файла – отдельная операция. А замен в одном файле может быть много

<operation error="skip|abort">
    
</operation>

не обязательный атрибут error указывает на действие которое нужно выполнить при не нахождении искомого: skip пропустить текущую операцию, abort – прервать модификацию вовсе. Тег search указывает на искомую строку/регулярное выражение

<search trim="true|flase" index="0|1|2...">
    <![CDATA[строка/регулярка]]>
</search>

атрибут trim(опционален) отвечает за игнорирование пробелов, index – также необязателен, указывает на номер вхождения(если их несколько).

И наконец, сама строка для внедрения

<add position="before|after|replace" trim="true|flase" offset="0|1|2..">
    <![CDATA[строка]]>
</add>

здесь trim тоже что и ранее, position это действие: вставить до, после, или вовсе заменить; offset – смещение по строкам от вхождения.

Полный файл модификатора выглядит так

<?xml version="1.0" encoding="utf-8"?>
<modification>
    <file path="путь к файлу 1">
        <operation error="skip|abort">
            <search trim="true|flase" index="0|1|2...">
                <![CDATA[строка/регулярка]]>
            </search>
            <add position="before|after|replace" trim="true|flase" offset="0|1|2..">
                <![CDATA[строка]]>
            </add>
        </operation>
        ...
    </file>
    ...
</modification>

И еще, малость не забыл – если есть модификатор, но нет модуля – структура zip-архива все равно должна быть как у модуля, но папка upload пустая.

Пишем телеграм-бот

Telegram – широкоизвестный и популярный мессендждер-соцсеть в узких кругах, и этот круг быстро расширяется благодаря стараниям его конкурентов. Под конкурентами я имею ввиду “традиционные” социальные сети, которые внедрением(ужесточением) цензуры отталкивают многих… Но эту статейку я хочу посвятить не свободе слова(или вседозволенности, как ее некоторый называют), а созданию бот-программы.

О Telegram

Он возник как эксперимент П.Дурова с технологией MTProto – методом шифрования, созданным его братом Н.Дуровым. MTProto – криптографический метод проитокол передачи данных, основанный на симметричном шифровании и ряде других приколюх, предназначенных для безопасной передачи данных. Все это значит, что исходящее сообщение могут прочесть только тот, кто его отправил, и тот, кому(те, кому – если это чат или канал) оно было отправлено. Именно эта защищенность от третьих лиц(не дающая возможности точечной модерации) и дает все новые и новые обороты в популярности этого приложения. Что и говорить – даже Трамп, после его изгнания из Twitter`а и прочих заокеанских сетей, создал Telegram-аккаунт!

Бот это что?

Телега имеет два вида API: Telegram API и Telegram Bot API. Первое служит для постройки своего приложения, использующее внутреннюю инфраструктуру(сервера по-сути) Телеграма, эти методы реализует TDLib. Бот – это также программа, но уже размещенная на своем сервере; еще отличие в том, что он(точнее, его интерфейс) может быть во всех программах использующих сервера телеги(в приложении Telegram точно, может быть реализовано в прогах на TDLib).

Итак,можно говорить было бы о Telegram API, но ты пришол сюда, вероятно, узнать о ботах. Первым делом – его возможности:

  • Получайте индивидуальные уведомления и новости
  • Интегрируйте с другими сервисами
  • Принимайте платежи от пользователей Telegram
  • Создавайте собственные инструменты
  • Создавайте одиночные и многопользовательские игры

Да, это то что перечислено в доках телеги – а что ты ожидал?? Я остановлюсь на пункте 4 и создам форму для оформления оформления заказа на сайте(по-сути, можно сказать пункт 2)

Создание бота. Теория

В этом разделе указана теория потому, что здесь я опишу алгоритм образования бота, так сказать – создать общее понимание процессов.

Прежде всего, как и при любой работе с программными интерфейсами, – аутентификация. Здесь не привычной аутентификации вроде получения открытого и закрытого ключей и, в начале каждой сессии, получение временного токена; вместо этого нужно написать боту, который, на основании предоставленных данных, зарегистрирует нового бота и выдаст тебе постоянный токен. После этого, в “диалоге” с ним же, выкатываем ему ввесь список команд, которые должны быть у бота.

Далее, мы должны сообщить адрес на который будут приходить все обновления – все действия с ботом -это вариант №1. Второй вариант – когда прослушка не установлена – все действия сохраняются на сервере мессенджера и, в течении 24 часов, можно получить этот лог. Эти два варианта – два взаимоисключающие методы: если установлена прослушка – лог на сервере вестись не будет и наоборот, но тогда события никуда не будут отправлены.

Создание бот. Практика

На практике всегда стоит использовать готовые, одобренные разрабами телеграма, библиотеками – об этом говорю не только я, но и документация, иначе может последовать бан. Но, дабы лучше понять, как это устроенно и работает – я реализую это нативно(без использования библиотек).

@botFather

Находим @BotFather, начинаем с ним переписку(отправляем команду “/start”). В ответ он перечислит все команды, которые ему доступны,, жмем “/newbot” и пишем слаг создаваемого бота. Если все прошло успешно(выполняется условие уникальности слага) – создается бот и ты получишь его токен; также, выполнив команду “/setcommands” можно указать список нужных команд(команда “/start” будет создана автоматически).

Перейдем непосредственно к программированию сервера нашего бота! Писать я буду на PHP, но разница невелика. Использую метод curl(), поскольку для задания пути нашего “слушателя” нужно отправить публичный ключ ssl-сертификата сервера с бот-программой. “Программного приветствия” нет, вот код для установки слушателя(его также можно установить из CLI)

<?php
    // инициализируем сеанс связи с нашим ботом
	$ch = curl_init( "https://api.telegram.org/bot{bot-token}/setWebhook" );
	// в заголовках сообщаем, что передаем файл
	curl_setopt( $ch, CURLOPT_HTTPHEADER, [ "Content-type: multipart/form-data" ] );
	curl_setopt( $ch, CURLOPT_POST, true );
	curl_setopt( $ch,
		CURLOPT_POSTFIELDS,
		[
			'url'         => 'https://'.$_SERVER['HTTP_HOST'],//  адрес слушателя
			'certificate' => '@' .realpath(__DIR__ . '/public.pem')// путь к публичному ключу
		] );

	curl_exec( $ch );

	curl_close( $ch );

В ответ этому скрипту, если не возникло ошибок, будет возвращена сторка

{"ok":true,"result":true,"description":"Webhook was set"}

но, на самом деле, это еще не показатель правильной установки адреса прослушки. Чтоб проверить правильность перейди по адресу https://api.telegram.org/bot{bot-token}/getWebhookInto, в случаи если все правильно будет

{"ok": true, "result": {"url": " https://xxx.xxx.xxx.xxx", "has_custom_certificate": true, "pending_update_count": 25, "last_error_date": 1484557151, "last_error_message" ":" Время ожидания соединения "," max_connections ": 40}}

В случаи, если интернет провайдер сервера твоего бота не блокирует Телеграм(намек на РФ), на адрес https://xxx.xxx.xxx.xxx будут приходить уведомления от бота. Например, пользователь сделал команду “/start”, на адрес https://xxx.xxx.xxx.xxx/start будет отправлен запрос.

В ответе на обновление от телеги должен содержатся один из “Допустимых типов”.

Погворим о втором варианте – получение обновлений самостоятельно. Прежде всего, нужно удалить прослушку: перейдите по адресу https://api.telegram.org/bot{bot-token}/setWebhook, при этом ты увидишь

{"ok":true,"result":true,"description":"Webhook was deleted"}

далее, любое действие с ботом будет записано и доступно по адресу https://api.telegram.org/bot{bot-token}/getUpdates в виде обьекта Update.

Как я говорил ранее, не делай этот код нативно используй одобренные разрабами библиотеки… все это лишь для понимания того, что происходит “под капотом” у этих библиотек! Оно пригодится…

PageSpeed Insights. Оптимизация страниц сайта под скорость его загрузки(на примере WordPress)

В современном интернете сайтов очень много, и, практически никогда, их не ищут имея прямой адрес, а находят в поисковиках. Ввиду этого, публичные сайты должны выбрасываться в верхних строках результатов поиска. Помимо релевантного запросу контента страницы и SEO, на это влияет время загрузки страницы. Именно его мы и будем сегодня уменьшать.

Содержание

PageSpeed Insights

Это один из многих анализаторов времени загрузки страницы. Анализатор(а не измеритель) потому, что он

PageSpeed Insights

оценивает время загрузки в своих “попугаях”(если об основной оценке).

Несмотря на это, этот инструмент является основным для проверки оптимизации сайта; как мне кажется – потому, что разработан Google`ом(ну и удобство – кратко и по делу, в многих альтернативных ресурсов наоборот – много, подробно и хз что с этим делать).

Исходная точка

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

С сайтами это эмпирическое правило сохраняется; с чего можно предположить 2 варианта: дабы уменьшить время загрузки – уменьшай контент страницы, либо же увеличивай скорость ее загрузки.

И здесь мое размышление(к делу не относящееся):

Принято говорить: “оптимизация страницы приводит к ускорению загрузки сайта”. Моя мысль: может ли руль изменить характеристики двигателя?..

Итак, к чему это? А не к чему! Если соблюдать терминологию(из мысли) то: двигатель это девайс пользователя(т.е. твой или сервера поисковика…), сайт – это, нет , не машина… это водитель! страница это машина. Что я хочу сказать – оптимизация сайта – это обучение водилы крутить баранку, проходить виражи с минимальной потерей времени. Да, главным двигателем здесь является сервер, на котором размещен сайт и который рендерит страницы – однако, в большинстве случаев, его оптимизировать мы не сможем.

Лады, перейдем к сути. На данный момент(перед началом оптимизации) я имею такие показатели:

27 попугаев
показатели перед стартом

Еще я забыл упомянуть – я буду ориентироваться на показатели для мобильных устройств. Объясню это следующим образом: производя поиск(даже если у пользователя ультра-супер-пупер ПК, мощности которого, все равно, не задействуются в поиске), первыми будут подняты те страницы, которые не только более соответствуют запросу, но и для поднятия которых требуется меньше ресурсов. К примеру, эта страница по показателям ПК имеет 77 попугаев(уже! без оптимизации!).

Чем крут PageSpeed еще – это объяснением, как оптимизировать. Вот мои проблемы

Начальные проблемы
Начальные проблемы

В принципе, это полный спектр проблем, которые может иметь сайт… а нет, вру! На одном сайте было “Сократите время ответа сервера”, поскольку к серверу никак притронуться я не мог – я переключил только версию PHP на 7.0(она дала наибольшее ускорение).

Я пошарился по странице(а именно в инструментах разработчика в хроме) и нашел основные тормоза. В первых, я использую библиотеку ace.js для раскрашивания кода, во-вторых – как и на многих сайтах, есть jquery; также, есть скрипты темы, скрипты для соцсетей и много стилей.

Первая беда – “Устраните ресурсы, блокирующие отображение”

Чтобы понять суть этой проблемы – нужно понять как браузер рендерит страницу: Этап 1 – загрузка HTML-разметки. Затем происходит парсинг и выстраивание DOM(Document Object Model); на этой стадии, каждый внешний файл вызывает блокировку процесса до момента полного его прочтения и осознания. Файлы JS блокируют выполнение во всех браузерах, CSS же(по словам разработчика Yahoo! – Stoyan Stefanov) не блокируют в серии IE браузеров, не блочат в Firefox.

Однако, перейдем к проблеме – открыв подробности о ней, виден совет:

Некоторые ресурсы блокируют первую отрисовку страницы. Рекомендуем встроить критическую часть данных JS/CSS в код HTML и отложить загрузку остальных ресурсов. Подробнее…

Скажу более того, здесь же и ссылка на плагины для WordPress, которыми можно оптимизировать этот(да и прочие) параметры. НО! Используя их, мне не удалось побороть эту проблему – по-этому я прибег к кодингу.

Начнем-ка! в начале,прежде чем что-то ломать, глянем на вкладку Coverage(если отсутствует – кликни по трем точкам у начала панели) в Google Crome DevTools

chrome web dev tools

с целью увидеть, что используется из активов. С ходу увидим в каких файлах использование кода равно 0 – в колонке “Unused Bytes” показатель 100%. в. Здесь ты можешь предположить, что я буду их снимать с регистрации, да! – именно так я и поступлю! К примеру, у меня нулевой файл имеет такую ссылку /wp-content/plugins/contact-form-7/includes/css/styles.css; находим стпоку в исходном коде страницы

<link rel='stylesheet' id='contact-form-7-css'  href='/wp-content/plugins/contact-form-7/includes/css/styles.css?ver=5.4' media='all' />

id=’contact-form-7-css’, что значит слаг – ‘contact-form-7’ и тип файла стили. Это знание позволит мне снять этот файл с регистрации на нужной мне странице

    add_action('wp_head', function () {
        remove_action('wp_head','wp_print_styles',8);  
        remove_action('wp_head','wp_print_head_scripts',9); 
        
        if(621 != $post->ID){
            wp_deregister_style('contact-form-7');
            wp_deregister_script('contact-form-7');
        }
        
        wp_deregister_style('rate-my-post');
        wp_deregister_style('twentyfifteen-style');
        wp_deregister_style('twentyfifteen-fonts');
    },1);

аналогично поступаю с прочими файлами. Также первыми 2-мя строками, перенесу подключение активов в подвал сайта.

Ну что – проверим результат?..

Общая оценка после первой итерации оптимизации
Общая оценка после первой итерации оптимизации
Оставшиеся проблемы по оптимизации
Оставшиеся проблемы по оптимизации

Результаты от сих действий ощутимы(меня они даже поразили!). Но не буду на этом останавливаться, продолжу развлечение…

Изврат с картинками

На деле здесь все просто – никакого извращения нет. По поводу современных форматов изображения, а именно .webp -дело в том, что не все браузеры его поддерживают. В связи с этим вместо такого тега для изображения

<img src="URL" alt="альтернативный текст">

для страниц с webp-картинками требуется такая структура

<picture>
   <source srcset="images/name.webp" type="image/webp">
   <img src="images/name.jpg" alt="описание" />
</picture>

при этом браузеру предлагается вначале загрузить webp, если он его не понимает – грузит “старый” формат. С практики скажу данная модификация дает крайне малый прирост, по этому я сделал альтернативный способ – ленивая загрузка изображений. Суть в том, чтоб браузер, загружая страницу, получал ее без картинок; по мере необходимости изображения подгружаются на javascript – этот метод дал мне большой прирост к скорости – я им и воспользовался. Единственная проблема – индексация картинок поисковыми роботами и, соответственно, можно забыть о трафике через изображения – но я решил, что переживу эту утерю… Итак, о самой загрузке – как же я ее сделал? Мой сайт на вордпресс, я поставил плагин Autoptimize… шучу, мне снего всего одна функция нужна, ее добавить одна(4) строчка

	add_filter( 'wp_get_attachment_image_attributes', function ( $attr ) {
		$attr['loading'] = 'lazy';

		return $attr;
	} );
<img src="URL" alt="альтернативный текст">

так выглядит тег картинки без лени

<img src="URL" loading="lazy" alt="альтернативный текст">

а так с ней. Чтобы добавить lazy-loading достаточно прописать атрибут loading=”lazy”. Однако, опять же, не все браузеры ее поддерживают и иногда нужно ее имитировать на js.

Результат

результат после второй итерации
результат после второй итерации

Согласись, не плохо – уже вплотную я приблизился к зеленой зоне(90). Кста, еще я установил плагин WebP Express для нарезки картинок в современном формате. Единственное замечание – для получения профита нужно в настройках выбрать “Replace image URLs”(вместо замены тегов).

Работаем с таблицами стилей

Здесь я буду использовать пока что мало распространенный способ – динамическое подключение. Сперва отключим стили вовсе

remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 );

при этом , также, отпадут скрипты(не беда). Следующим шагом – выведем в подвал скрипт, который после полной загрузки страницы вставит ссылки на таблицы стилей

        <script>
            window.onload = function () {
                var footer = document.querySelector('footer');

                footer.insertAdjacentHTML('beforeend', `<?php wp_print_styles() ?>`);
            }
        </script

window.onload вызывает функцию после загрузки и отрисовки страницы. На медленных машинах(или с медленным интернет-соединением) при этои будет посойное наложение стилей.

Оптимизируем скрипты

Результат, полученный мною, неплох – но недостаточен. Следующий шаг оптимизация скриптов. Первое, что я хочу сделать – подключать их асинхронно, т.е. параллельно прочим процессам. Чтоб это сделать нужно просто дописать атрибут async к тегу script

<script async="async" src='URL'></script>

вот как это выглядит в разметке, а вот как можно это сделать в коде для вордпресс

add_filter( 'script_loader_tag', function($tag, $handle){
    return str_replace( ' src', ' async="async" src', $tag );
}, 10, 2 );

От единого этого действия мои показатели улучшились(я считаю значительно)

результат после второй * итерации

но на этом со скриптами еще не все.

Далее, я пойду таким же путем, как и со стилями. Тут тоже такое работает, однако реализация отлична. Дело в том, что закрывающийся скрипт-тег в строке, закрывает скрипт(по крайней мере в многострочной строке). В связи с этим, я записал пути в массив и, создавая тег, вставлял ссылки на скрипты

// заполнение переменной to_do 
$wp_scripts->all_deps( $wp_scripts->queue );

foreach ( $wp_scripts->to_do as $handle )://echo $handle.'-'.$wp_scripts->registered[ $handle ]->src.'<br>';
	if ( ! empty( $wp_scripts->registered[ $handle ]->src ) ) {
		$scripts[] = $wp_scripts->registered[ $handle ]->src;
	}
        // вывод зависимого кода
	if ( $wp_scripts->registered[ $handle ]->extra ) {
		echo "n<script>n";
		echo "/* <![CDATA[ */n"; // Not needed in HTML 5.
		echo $wp_scripts->registered[ $handle ]->extra['data'];
		echo "/* ]]> */n";
		echo "</script>n";
	}

endforeach;

конвертирую все это в js

                var scripts = '<?php echo json_encode( $scripts )?>',
                    footer = document.querySelector('footer');

                JSON.parse(scripts).forEach(function (src) {
                    script = document.createElement('script'),
                        script.src = src;
                    script.async = false;

                    footer.append(script);
                })

Все скрипты, кроме ace.js, нормально работают. Немного просмотревуказанный скрипт понял, что проблема с его инициализацией. немного переделав его и подключив его напрямую(в смысле без динамического тега), и он тоже заработал. Результат страницы составил

результат после третей итерации
результат после второй итерации

Не идеально, но все же! Можно еще минификацию активов сделать, но они итак минифицированы. Ну, да и ладно…

Elementor Addon. Пишем кастомный виджет к билдеру

Elementor – популярный wp плагин для построения страниц; данный тип плагинов называют pagebuilder. Это плагин(как и все в его категории) оперирует специальными виджетами – именно его я и создам, по ходу статьи…

Содержание

Предисловие

Здесья расскажу о том, что толкнуло меня на создание виджета(помимо получения опыта для написания этой писульки). Дело было так: была страница на которую выводилось видео с YouTube. К нему был список тайм-кодов, клик по которым включал воспроизведение видео с определенного момента. Временная метка(таймкод) состоит из самого времени, картинки и текста-описания. При переезде страницы на Элементор – всё взаимодействие сламалось. Задача: создать аддон к Элементор, добавляющий эти возможности в виджете.

Немного воды…

Всем, кто знаком с веб-программированием, с ходу стало понятно в чем причина – javascript; теперь для тех, кто незнаком: за прослушивание всех событий в браузере отвечает javascript. И правда, билдер изменил структуру DOM-дерева страницы и, за часик(долго разбирался в новой структуре), изменив селекторы в скрипте все заработало…

И что ж?.. на этом все? Было бы всё, если бы владелец не сказал, что неудобно так: строить ряд, в нем колонки с текстовым полем в(которых снова ряды с колонками) – сделай-ка единым блоком, чтоб в элементы управления просто вбить значения.

Приступим…

Начал я, разумеется, с оболочки плагина. На сайте плагина, в доках, есть шаблон кода основного файла для аддонов Элементор. Немного опишу главный метод, который в нем присутствует

add_action('elementor/init', function () {
    add_action('elementor/widgets/widgets_registered', function () {
        require_once(__DIR__ . '/class-youtube-widget.php');
        ElementorPlugin::instance()->widgets_manager->register_widget_type(new Elementor_YouTube_Widget());
    });
});

чтоб написать любое расширение к элементору нужно расширить его класс; чтобы получить доступ к ядру плагина(а именно там лежат они) нужно вызывать аддон после загрузки плагина. В движке есть хук(plugins_loaded), срабатывающий после загрузки всех плагинов – однако, в виду того, как устроен сам Элементор – в этот момент его ядро уже недоступно.

Плагин Элементор имеет свои хуки, позволяющие получить доступ к разным классам его ядра:

  • elementor/init – ко всем;
  • ementor/widgets/widgets_registered – к классу ElementorWidget_Base(и прочим, необходимым для создания нового виджета);
  • elementor/controls/control_registered – к классу ElementorControl_Base(и прочим, необходимым для создания нового типа контролера для плагина);

Это все хуки, которые нужно знать, для того чтоб построить свой виджет. На деле, из-за довольно широкой палитры встроенных контролей, 3ий хук я никогда не использовал – не было нужды создавать свои контроллеры.

Теперь можно переходить к созданию самого виджета… хотя, нет – пару строк о скрипте. Здесь, как и в вордпресс, нет метода, который подключал бы скрипты только когда виджет есть на странице(по крайней мере я таких не увидел – если ты о них знаешь – напиши в комментах, буду благодарен!) и потому я подключаю их как обычные скрипты.

Теперь немного о самом скрипте – в нем я буду использовать YouTube Iframe Player API. Начнем с html-разметки, которую будем строить на странице

        <section>
            <h2>{{ section_title }}</h2>
            <div class="elementor-row {{ video_position }}" data-id="{{ video_id }}">
                <div class="elementor-column frame-container">
                    <img src="https://img.youtube.com/vi/{{ video_id }}/hqdefault.jpg" class="youtube-preview" alt="Video Preview">
                </div>
                <div class="elementor-column">
                    {{ timestamps_loop }}
                        <figure class="stamp">
                            <img src="{{ timestamp_image_url }}" alt="Stamp Icon">
                            <span class="timestamp">{{ timestamp_time }}</span>
                            <span class="description">{{ timestamp_description }}</span>
                        </igure>
                    {{ endloop }}
                </div>
            </div>
        </section>

Думал использовать Iframe API, но глянув, что он делает – послал его лесом. Дело в том, что беглый анализ кода показал, что апи содержит методы постройки тега iframe с его атрибутами; учитывая, что страница индексируемая(должна быть оптимизирована под скорость загрузки) я решил не захламлять ее дополнительной библиотекой(jquery и так хорошо выполняет эту задачу). В общем, вот скрипт для работы с метками с вышеприведенной разметкой

(function($) {
    'use strict';

    $(document).on('click', '.youtube-preview', function() {
        var id = $(this).closest('.elementor-row').data('id');

        YTFrame.build(id, 0, $(this).parent());
    });

    $(document).on('click', 'figure.stamp', function() {
        console.log(this);
        var id = $(this).closest('.elementor-row').data('id'),
            time = $(this).find('.timestamp').text().split(':'),
            seconds = 60 * parseInt(time[0]) + parseInt(time[1]);
        console.log($(this).closest('.elementor-row').find('.frame-container')[0]);

        YTFrame.build(id, seconds, $(this).closest('.elementor-row').find('.frame-container')[0]);

    });

    var YTFrame = {
        build: function(id, seconds, container) {
            var iframe = document.createElement('iframe');

            iframe.src = 'https://www.youtube-nocookie.com/embed/' + id + '?autoplay=1&start=' + seconds;
            $(container).html(iframe);
        }
    }
})(jQuery);

Также, данный скрипт позволил обойти некоторые ограничения.

Доделываем

Имея скрипт и накидав разметку, а также оболочку для аддона – создадим класс нашего виджета. Он является дочерним для базового абстрактного виджета элементор(который доступен только в ядре плагина); также, необходимы классы для контроля виджета в редакторе

<?php

use ElementorControls_Manager;
use ElementorWidget_Base;
use ElementorUtils;
use ElementorRepeater;

Теперь создадмим сам класс со следующими методами

<?php
class Elementor_Test_Widget extends ElementorWidget_Base {

	public function get_name() {}

	public function get_title() {}

	public function get_icon() {}

	public function get_categories() {}

	protected function _register_controls() {}

	protected function render() {}

	protected function _content_template() {}

}

Первые 4 методы просто возвращают по строке. Метод render() строит разметку в редакторе(с приметкой) и на сайте

    protected function render()
    {
        $settings = $this->get_settings_for_display();
        $this->add_inline_editing_attributes('title', 'advanced'); ?>
        <section>
            <h2 <?php echo $this->get_render_attribute_string('text_attr') ?>>
                <?php echo $this->get_settings('title') ?>
            </h2>
            <div class="elementor-row  <?php echo esc_attr($settings['video_position']) ?>" data-id="<?php echo esc_attr($settings['video_id']) ?>">
                <div class="elementor-column frame-container">
                    <img src="<?php printf('https://img.youtube.com/vi/%s/hqdefault.jpg', esc_attr($settings['video_id'])) ?>" class="youtube-preview" alt="Video Preview">
                </div>
                <div class="elementor-column">
                    <?php foreach ($settings['timestams_repeater'] as $key => $value): ?>
                        <figure class="stamp">
                            <img src="<?php echo esc_url($value['stamp_image']['ur']) ?>" alt="Stamp Icon">
                            <span class="timestamp"><?php echo esc_html($value['time_stamp']) ?></span>
                            <span class="description"><?php echo esc_html($value['stamp_description']) ?></span>
                        </igure>
                    <?php endforeach; ?>
                </div>
            </div>
        </section>
        <?php
    }

Здесь Widget_Base->get_settings_for_display() это получение настроек виджета, которые я создам в следующем методе.

Метод _register_controls() отвечает за создание контролей виджета в сайдбаре редактора – здесь немного поподробней. Все настройки растоложены в 3х табах – контент, стиль и дополнительные(их не трожь – они всегда дефолтные!). чтоб выбрать таб нужно в этом методе написать

        $this->start_controls_section(
            'content_section',
            [
                'label' => __('Video'),
                'tab' => Controls_Manager::TAB_CONTENT,
            ]
        );

затем следует открывающий и закрывающий секцию настроек код

        $this->add_control(
            'title',
            [
                'label' => __('Section Title'),
                'type' => Controls_Manager::TEXT,
                'input_type' => 'text'
            ]
        );

        $this->end_controls_section();

и уже между этими тегами можно писать свою настройку.

Вся “слоенка” будет выглядеть так

        // указание на таб
        //  если TAB_CONTENT поменять на TAB_STYLE
        // будет запись в стиль-таб 
        $this->start_controls_section(
            'content_section',
            [
                'label' => __('Video'),
                'tab' => Controls_Manager::TAB_CONTENT,
            ]
        );

        // открытие новой секции
        $this->add_control(
            'title',
            [
                'label' => __('Section Title'),
                'type' => Controls_Manager::TEXT,
                'input_type' => 'text'
            ]
        );


        // настройка в секции, их может быть много
        $this->add_control(
            'video_id',
            [
                'label' => __('Video ID'),
                'type' => Controls_Manager::TEXT,
                'input_type' => 'text'
            ]
        );

        // закрытие текущей секции, но не таба!
        // после этого можно открыть новую секцию
        // она будет в том же табе
        $this->end_controls_section();

Впринципе, этих методов достаточно для того, чтоб сделать рабочий виджет. Далее пойдет вещь, которую я не делал в этой работе, но хочу показать и рассказать.

Наверняка внимательный читатель не только статьи но и кода, заметил в рендеринг-методе эти строки

$this->add_inline_editing_attributes('title', 'advanced');
$this->get_render_attribute_string('title');
$this->get_settings('title');

все они не имеют смысла без метода _content_template(). Поясню: виджет элементор – блок визуально редактируемый; это значит его можно редактировать в поле редактора, а не в сайдбаре. Кароч, 1ая строка это указатель на то, что эта настройка имееит возможность визуального редактирования; 2ой – получает набор классов для того чтоб javascript элементора взял под контроль этот тег; 3я – получает само значение настройки. В случаи с ‘advanced’ можно увидеть расширенную панель редактирования текста

расширенная панель редактирования текста
расширенная панель редактирования текста

Однако, чтоб все это сработало – необходим вышеуказанный метод с следующими строками

<# view.addInlineEditingAttributes( 'title', 'advanced' ); #>
<div {{{ view.getRenderAttributeString( 'title' ) }}}>{{{ settings.title }}}</div>

Заметь(!) эти строки должны выводиться, тк они будут вставлены в backbone-шаблоны(документация по ним). 2ая строка это т, что в окне редактора будет вставлено вместо редактируемой строки.

Заключение

Было разработанное расширение для Элементор. Код сможешь глянуть здесь, конечно он не полный(я удалил из него интеграции с другими плагинами, кастомными плагинами и сократил дерево настроек). Больше примеров ты увидишь в самом Элементор, ну а я попытался немного объяснить – что и как там работает.

Блокчеин, крипта и майнинг – технический взгляд на вопрос

Начало 2021 года… курс биткоина перевалил 40К долларов…. однако здравствуйте! В связи с очередной волной криптохайпа на рынке, решил я начеркать эту статейку – о крипте с технической стороны. С финансовой стороны(точнее мое, обывательское, отношение к ее ценности) – крипта такой же финансовый инструмент, как и доллар США. Объясню: ныне, доллар это цифра, сила которой обеспечена гарантиями правительства(системы) США. Крипта – это такая же цифра, обеспечена гарантиями системы, в которой она хранится – блокчейном. Ценность любого ресурса, в конце-концов, определяют люди. На этом, перейдем к сути статьи…

Содержание

Технический базис

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

  • независимо от длины входящей строки – хеш-код имеет строго определенную длину;
  • имея хеш невозможно восстановить исходную информацию;
  • результат хефирования идентичных строк идентичный;
  • один хеш может иметь только одну исходную строку.

Как, я думаю всем понято, последнее утверждение, теоретически, бессмысленное; поскольку, пул символов ограничен и их количество в хеш-строке также. Но я сказал теоретически – поскольку это число(вариантов комбинаций) велико(для алгоритма SHA-256, в биткоин, их аж 2 в 256-ой степени).

Блокчейн

Блокчейн – это, по большому счету, всего лишь база данных(ну или бухгалтерская книга, коль разговор в контексте криптовалюты). Да, в связи с особенностями его использования – она окружена различными проверками входящих данных. В виду ее безопасности(об этом позже) я б, к примеру, использовал данную схему для проведения электронных выборов; но на данный момент, ничего лучше, чем хранить крипту или свои идентификационные данные, люди не придумали(кста, го в комменты – почему так?).

Предлагаю(а точнее – настаиваю, раз уж ты это читаешь) разобраться, как она устроена и что из себя представляет.

Теоретический взгляд

Данная схема хранения данных предусматривает зависимость каждого следующего блока данных от предыдущего, с английского – “цепочка блоков”. Кроме того, для пущей безопасности, блокчейн делают децентрализованным – т.е. копия БД и сопровождающее ее ПО хранятся у каждого, кто использует данную сеть, и все они равноправны.

Децентрализованный блокчейн могут подменить только >50% пользователей этой системы, с одинаково сфальсифицированными блоками.

Майнинг

Процесс майнинга это обычный процесс “угадывания” комбинации знаков методом перебора, которые, после определенных манипуляций должны соответствовать определенным требованиям. По сути, это брутфорс паролей…

Теория

В целом, майнинг с точки зрения блокчейна – это процесс упаковывания транзакций в блоки; для майнера же, это процесс добычи крипты + получение комиссии с каждой упакованной операции.

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

Рассмотрим на примере Биткоина. Первоначально, с полученных транзакций формируется дерево Меркла(о нем – ниже); после, берется корень этого дерева, конкатенируется с хешом предыдущего блока и случайной строкой(nonce-field); из полученной строки создается хеш и проверяется его сложность: если она выше, чем требует блокчейн для нового блока – составляется новый nonce-field и процесс повторяется, в ином случаи – создается новый блок, о чем оповещаются все майнеры.

Биткоин-блокчейн пересчитывает сложность хеша каждые 2016 блоков таким образом, чтоб процесс нахождения nonce-field(а соответственно, и создание блока) был равен 10 минутам.

Дерево Меркла

Дерево Меркла – это способ хранения информации в виде хешей. Оно строится так: каждая транзакция хешируется – таким образом, формируются листья первой очереди. Затем хеши соседних листьев складывают(по 2) и снова получают хеш этой строки. Эту операцию проделывают до тех пор пока не остается 1 строка – это и есть корень дерева Меркла.

дерево Меркла
дерево Меркла

Как ты заметил(наверное), листьев должно быть четное количество – по этому, если транзакций нечетное количество, последний лист дублируется.

Так зачем же оно надо? Дело в том, что каждая транзакция, после попадания в блок, находится в “замороженном” состоянии. Она получает окончательное одобрение системой только после нескольких подтверждений. Это подтверждение блок транзакций получает при постройке следующих блоков, в которые, замороженная транзакция также вносится; к примеру, в биткоине, для признания транзакции вырлаты награды полностью легальной, нужно 100 подтверждений(последующих блоков), а на оплату биткоинами 6. Так вот, каждая проверка подтверждения требует усилий сети, а такая структура их экономит. Кроме экономии вычислительных мощностей, эта структура позволяет строить “легкие” клиенты – хранящие только корни дерева.

Данные подтверждения, как я понял, нужны для предотвращения двойных трат средств.

Сказ о сложности

Дело в том, что хеш, который мы видим – это представление в шестнадцатеричной форме; его можно перевести в десятеричную – и тогда это будет обычное число. Сложность это разница между максимальным значением хеша и текущим хешем. Строка с максимальной сложностью это, по сути, шестнадцатеричная строка, состоящая с самого старшого символа в этом формате и переведенная в десятичный. Допустимыми символами в шестнадцатеричной системе есть десять цифр(0 – 9) и шесть первых букв латинского алфавита(a – f), таким образом – старший символ f. Формула перерасчета выглядит так

где d – порядковый номер символа(0 – 15), n – порядковый номер символа в строке.

Вот почему, когда говорят о сложности и цели в майнинге биткоин, большинство статей упоминают о предшествующих нулях в хеше – ведь они не вносят никакого влияния на величину хеша.

О майнинг-пулах

В начале, когда суммарные вычислительные мощности сети были малы – у каждого были шансы(они и сейчас есть) найти нонс–строку. Но когда крипта “выстрелила” и майнинг стал бизнесом, игроки стали обьеденять усилия для повышения своих шансов. Так умер соло-майнинг, потому как в кооперации можно договориться о разделении проверяемых значениях.

Кошелек и электронная подпись

Каждое перечисление или начисление крипты происходит с/на кошелек. Адрес кошелька это, по сути, хеш от каких-то персональных данных(либо, как у биткоин-кошельков, набор случайных символов) владельца. Однако, ввиду того, что блокчейн(как и любая компьютерная система) построен на недоверии – каждая транзакция требует подписи. Здесь и вступают на сцену публичный и секретный ключи. Работают они так: каждая транзакция содержит подпись(шифровку от хеша транзакции и секретного ключа) и публичный ключ; майнер, прежде чем внести новую информацию в блок, при помощи публичного ключа, дешифрует подпись; если хеш операции равен хешу после дешифрования – все ок.

Ремарка о подписи: в процессе шифрования используют хеш информации по причине экономии вычислительных мощностей.

По поводу баланса кошелька – система не хранит информацию о нем; он результат всех транзакций по адресу.

Что же такое криптовалюта?

Криптовалюта – это вознаграждение майнеру за формирование нового блока, т.е. за поддержание блокчейн-сети в рабочем состоянии. Оно состоит из фиксированной суммы(установленной сетью) и комисии с транзакций(также установленной сетью).

Например, в сети Биткоин, вознаграждение за первые 210 тыс. блоков было по 50 монет; спустя каждые 210 тыс. оно делится на 2. Примерно, к 2033 году эмиссия новых монет прекратится, т.е. майнеры будут получать только комиссию. И-да, ответить – как же их увидеть? – первая транзакция в начале каждого тела блока.

Заключение

Здесь я кратко прошёлся по основным моментам крипты и всем, что с ней связано. В одной из будущих статей реализуем свою валюту; не в виде токена на существующем блокчейне, а создадим с ничего… В общем, to be continue…

WebSocket’ы – что и как? реализация на PHP

Приветствую всех, кто заглянул ко мне на огонек! Сегодня речь пойдет о вебсокетах. Если ты пользовались мессенджерами, чатами, играли в онлайн-игры или смотрели прямые трансляции – ты однозначно были клиентом вебсокет-соединения.

Содержание

Теория

WebSocket — протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени. Протокол управления передачей(TCP) основной протокол передачи данных в интернете; он является подложкой для остальных протоколов, среди которых сравним http/https и ws/wss. Обычно, “общаясь” с сайтом, браузер использует http/https протоколы, которые работают в режиме “вопрос-ответ”, т.е. без запроса от браузера никакой информации от сервера не будет. Вебсокет протокол(ws и wss, защищенный, по аналогии с http/https), в свою очередь, держит “коридор” обмена данными постоянно открытым, т.е. сервер может по “своей” инициативе послать информацию.

“Рукопожатие”

Это, пожалуй, основное событие в вебсокет-общении. И несмотря на это, оно незаметно и никак не зависит от клиента(имеется ввиду – от человека в чате, либо другом приложении). Происходит оно так: клиент отправляет заголовки серверу

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

с предложением сменить протокол на websocket, вместе с этим присылает ключ. На основании ключа, сервер строит свой ключ и отсылает его в виде заголовков клиенту. Если полученный клиентом заголовок

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

содержит код ответа 101 и правильный ключ – ответ воспринимается как “да”. Сэтого момента соединение установлено и общение может быть начато, но если сервер не пришлет заголовков или будет неверен ключ – соединение будет разорвано.

Правила построения ответного ключа:

  • взять строковое значение из заголовка Sec-WebSocket-Key и объединить со строкой 258EAFA5-E914-47DA-95CA-C5AB0DC85B11;
  • вычислить бинарный хеш SHA-1 (бинарная строка из 20 символов) от полученной в первом пункте строки;
  • закодировать хеш в Base64.

Немного о передаваемых данных

Согласно спецификации RFC 6455 обмен данными происходит в виде фреймов. Вот блок-схема каждого фрейма

структура вебсокет-фрейма

С первого взгляда вспомнился мем “Ну нахер…”, ну да ладно! Из этой схемы и ее описания можно сделать заключение:

  • каждая “порция” информации передаётся фреймами, она может быть в одном или нескольких подряд;
  • каждый фрейм начинается с информации о том, как извлечь целевую информацию из него.

Теперь о самих правилах построения фреймов…

“Ингредиенты” фреймов

Каждый фрейм строится по следующих правилах:

  • Первый байт указывает на то, полная ли в нем информация(1), или будет продолжение(0). В случаи если фрейм обладает не полной инфой – нужно ждать закрывающего фрейма, в котором первый бит будет равен 1;
  • следующие 3 бита(обычно по 0) это расширение для протокола;
  • следующие 4 бита определяют тип полезных данных фрейма:
    • 0х1 – текстовые данные;
    • 0х2 – бинарные(файл);
    • 0х3-7- окно возможностей для полезной информации на будущее(сейчас таких данных нет);
    • 0х8 – фрейм с приказом закрыть соединение;
    • 0х9 – фрейм PING на проверку состояния соединения;
    • 0хА – фрейм PONG(ответ на PING, говорящий “все ОК”);
    • 0хB-F – окно возможностей для управления соединением на будущее(сейчас таких данных нет);
    • 0х0 – фрагментированный фрейм, являющийся продолжением предыдущего.
  • следующий бит(маска) указывает замаскирована ли инфомация фрейма;
  • следующие 7 бит или 7 бит + 2 или 8 байта(сейчас объясню) это длинна тела сообщения. Если эти 7 бит перевести в значение, то правила следующие:
    • если значение между 0 и 125 – это и есть длинна тела сообщения;
    • когда значение 126 – на длину тела указывают следующие 2 байта(16 бит);
    • значение строго больше 126 – на длину тела указывают следующие 8 байта(64 бит);
  • если маска установлена, 4 байта после длинны тела будут ее ключом – в ином случаи(маска неустановленная, т.е. 0) – этого слоя в фрейме не будет;
  • и в конце, в оставшемся будет содержатся полезная информация фрейма.

Эту информацию нужно знать для того, чтоб правильно составить функцию кодирования/декодирования информации сокета. Далее, я построю и эти функции.

Немного о битовых масках

Битмаски – это последовательность битов, предназначенных для маскирования целевой информации. Не буду излагать всю теорию о них(если интересно – загуглите или напишите в комментах), меня, в контексте вебсокетов, интересует лишъ одна операция – раз маскирование целевой информации методом xor(именно он указан в спецификации на протокол вебсокет)

Использование битмаски, механизм xor

Пусть верхний ряд цифр это битовая маска, а средний – скрытая информация. Будем рассматривать столбцы цыфр. Следуя операции xor нужно сравнить бит маски с соответствующим битом скрытой инфы, и если они совпадают, то результат false(0 у выражении состояния бита) и наоборот. Проделав эту операцию со всеми битами(длина маски = длине полезной информации) будет получена строка с исходными данными. Ключом маски называется наименьший повторяющийся участок маски – именно его хранит каждый фрейм.

Балабольство о практике

Как следует из ранее сказанного – для соединения нужно иметь сервер и клиент. На сервере будет запущен демон(никакого отношения к мифологии, всего-лишъ – вечно работающий скрипт), а клиент будет будет веб-страницей.

В идеале, сервер должен быть написан на языке программирования, поддерживающем асинхронность, например серверный javascript(node.js). Но, допустим, это пристройка к проекту на PHP, среди доступных разработчиков нет(я о тебе, т.к .полагаю, что ты разраб) обладающих знаниями или не желающий изучить новое(что странно); или еще какая-то ведомая лишъ тебе причина. Как известно, пых пока что полностью синхронен, но для низко нагруженных проектов – этого достаточно – выльется только в задержки работы; для нагруженных проектов есть фреймворки, обеспечивающие асинхронность – reactPHP, amPHP, для вебсокет-серверов с асинхронностью – workerman, ratchet и т.д.

По ходу статьи я планирую реализовать следующее(на нативном php):

  • websocket-сервер на PHP;
  • HTML/JS-клиент для общения с сервером.

На деле и пых может быть клиентом и слушать сокет…

Реализация серверной части

Придумываем задачу. Сервер будет принимать сокет-соединение, приветствовать нового пользователя и сообщать о нем ранее присоединившихся. Любой может “убить” сокет-сервер.

Начнем реализацию, а именно под случай, когда нет доступа к серверу по SSH и запустить скрипт можно вызвав его в браузере. Для этого вначале снимем ограничение времени работы скрипта и запретим завершать работу после закрытия браузера

<?php
ignore_user_abort(true);
set_time_limit(0);

далее, создадим вебсокет, к которому и будут присоединятся клиенты

<?php
if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP))) {
    echo 'Сокет не создан. Причина: ' . socket_strerror(socket_last_error());
}

здесь сокет создается функцией socket_create(), которая при создании сокета возвращает сокет ресурса, иначе false; конструкция socket_strerror(socket_last_error()) всего получает текст ошибки, параметры это семейство протоколов, тип передачи данных и используемый протокол. Кста, забыл главное – проверь конфигурацию пыха – чтоб  --enable-sockets был true. Сейчас нужно привязать сокет и выставить его на прослушку, а также, сделать его неблокирующим

<?php
if (socket_bind($sock, 'localhost', 8080)) {
    echo 'Сокет не привязан. Причина: ' . socket_strerror(socket_last_error());
}

if (socket_listen($sock, 10)) {
    echo 'Сокет не прослушивается. Причина: ' . socket_strerror(socket_last_error());
}

socket_set_nonblock($sock);

здесь первый – сокет созданный ранее, ‘localhost’ – домен или IP адрес сервера, 8080 порт, через который будет открыт доступ. В socket_listen() второй параметр необязателен, будет указывать максимальное количество подключенных к нему клиентов.

На этой стадии, я имею сокет; чтоб превратить его в вебсокет – нужно запустить его как службу, т.е. в демон(цикл событий). В самом простом случаи сгодится такой код

<?php
while(true){
    // дальнейшая работа
}

Итак, первым, что нужно сделать – проверить наличие новых подключений(дальнейший код в этой главе должен быть расположен в цикле)

<?php
if ($connection = socket_accept($sock)) {
   $headers = socket_read($connection,1024);
}

При их наличии, прочтем их(получив при этом заголовки от клиента) и, исходя из ранее изложенных правил, сформируем серверные заголовки

      $parts = explode('Sec-WebSocket-Key:',$headers); 
      $secWSKey = trim(explode(PHP_EOL,$parts[1])[0]);
      $secWSAccept = base64_encode(sha1($secWSKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));

      $answer = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' . $secWSAccept,
        'Sec-WebSocket-Version: 13'
      ];

      if (stripos($headers,'name=') !== false) {
        $parts = explode('name=',$headers,2);
        $name = explode(' ',$parts[1])[0];
      }
      $name = isset($name) ? $name : 'Anonymous' . count($connections);

      socket_write($connection, implode("rn",$answer) . "rnrn");

которые запишем в сокет(здесь замечание – если заголовки отправить без пустой строки, клиент не поймет, что это конец и будет ожидать продолжения заголовков). Таким образом мы совешим “рукопожатие” – с этого момента установлено полноценное вебсокет-соединение между сервером и клиентом(ах-да – чтоб ему в дальнейшем отправлять сообщения – запишем его коннект в массив). Ну, и необязательная

      socket_write($connection,encodeToFrame('Server: Hello! Welcome to Chat, ' . $name));

      if (!empty($connections)) {
        foreach ($connections as $connect) {
          socket_write($connect->connection,encodeToFrame('Server: New User(' . $name . ') in Chat!'));
        }
      }

      $connections[] = (object) [
        'connection' => $connection,
        'name' => $name
      ];

программа – поприветствуем новенького, оповестим остальных о нем и запишем “контактные данные”(функции кодирования и декодирования рассмотрим отдельно). Малость не забыл, при передачи параметров методом GET, они будут доступны только в заголовке.

После проверки на новых – проверим каждый сокет на наличие фреймов, если они есть – декодируем их

    if (!empty($connections)) {
      foreach ($connections as $connect) {
        $message = frameDecode(socket_read($connect->connection,1024000));

        if ($message === 'break') {
          break 2;
        }

        if (!empty($message)) {
          foreach ($connections as $c) {
            socket_write($c->connection,encodeToFrame($connect->name . ': ' . $message));
          }

          $message = '';
        }
      }
    }

и отправим сообщение всем участникам. Поскольку цикл “живет” пока работает сервер сделаем возможность принудительной его(цикла) остановки. После выхода из цикла событий закроем сокет

socket_close($sock);

Декодирование фрейма

Здесь все просто – просто следуем правилам. Вначале, из полученной строки(в том, что это именно строка можно убедится распечатав ее var_dump-ом) фрейма извлечем первый и второй байты в двоичном виде(побитово)

  $firstByteToBits = sprintf('%08b', ord($frame[0]));
  $secondByteToBits = sprintf('%08b', ord($frame[1]));

извлечем информацию о типе полезной информации и ее длине

  $opcod = bindec(substr($firstByteToBits,4));
  $bodyLenght = bindec(substr($secondByteToBits,1));

  if ($bodyLenght < 126) {
    $bodyLenght = $bodyLenght;
    $maskKey = substr($frame,2,4);
    $body = substr($frame,6,$bodyLenght);
  } elseif ($bodyLenght === 126) {
    $bodyLenght = sprintf('%16b',substr($frame,2,2));
    $maskKey = substr($frame,4,4);
    $body = substr($frame,8,$bodyLenght);
  } else {
    $bodyLenght = sprintf('%64b',substr($frame,2,8));
    $maskKey = substr($frame,10,4);
    $body = substr($frame,14,$bodyLenght);
  }

если ее длинна хранится в следующих 16-ти или 64-х битах – извлечем следующие 2 или 8 байта(8 бит = 1 байт, кто забыл, а 1 байт = 1 символ строки) в битовой форме

Проверим кадр на целостность(фрагментирование), на тип информации и наличие маскировки(я буду пропускать фрагментированные, не маскированные и кары с нетекстовой информацией)

  if ((int)$secondByteToBits[0] === 0 || $firstByteToBits[0] === 0 || $opcod !== 1) {
    return '';
  }

О снятии маски – фрейм содержит 32-битный ключ и имеет смысл разбивать полезные данные данные участками по 32 бита, и уже их демаскировать(склеить конкатенацией)

  $i = 0;
  $unmaskedBody = '';

  while ($i < $bodyLenght/4) {
    $unmaskedBody .= substr($body,4*$i,4) ^ $maskKey;
    $i++;
  }

Если все же нужно обрабатывать и фрагментированный фреймы – стоит взглянуть в сторону глобальных переменных и конкатенации демаскированных строк.

Кодирование в фрейм

Эта часть оказалась немного сложнее, по крайней мере для меня – у меня образование не связанное с ЕОМ, а в документации об этом я ничего не нашел. Дело в том, что для шифрования длины – я должен понимать откуда 125, 126 и 127 ? Тут я сделал предположение

Имеется 7 бит, которые могут иметь 128(27) положений. Первое – это 0 символов(байт), 2 положения(126 и 127) это для информирования о том, какому правилу следовать. Исходя из этого, 126-ое положение свидетельствует о длине сообщения = 125 байт. Из этого можно сделать вывод – максимальная длина сообщения, содержащаяся в 16-ти битах = 216 – 1, в 64 = 264 – 1.

Подытожим все это в коде: определим значение 7-ми последних битов второго байта и, по необходимости, 2-ох или 8-ми последующих байтов

  $opcodInBits = sprintf('%04b', 1);
  $bodyLenght = strlen($content);
  $bodyLenghtInSecondByte = sprintf('%07b',$bodyLenght);
  $extendedLenght = '';

  if ($bodyLenght > 125) {
    if ($bodyLenght < 65536) {
      $bodyLenghtInSecondByte = sprintf('%07b',126);
      $extendedLenght = sprintf('%32b',$bodyLenght);
    } elseif ($bodyLenght > 65535 && $bodyLenght < 4294967296) {
      $bodyLenghtInSecondByte = sprintf('%07b',127);
      $extendedLenght = sprintf('%64b',$bodyLenght);
    } else {
      return '';
    }
  }

  $firstByte = chr(bindec('1000' . $opcodInBits));
  $secondByte = chr(bindec('0' . $bodyLenghtInSecondByte));

Несмотря на то, что протокол говорит о равных правах клиента и сервера, сервер требует маскированных данных, клиент – открытых.

Реализация клиентской части

Создадим простое текстовое поле и поле для сообщений

  <table>
    <tr>
      <td>
        <textarea name="name" rows="8" cols="80" id="input"></textarea><br>
        <button type="button" name="button" id="sendToServer">Send</button>
      </td>
    </tr>
    <tr>
      <td id="messages"></td>
    </tr>
  </table>

а далее, javascript-обьект для общения с серверным сокетом

    var socket = new WebSocket('ws://localhost:8080?name=Bogdan');

он имеет 4 метода, которые срабатывают при разных событиях

    socket.onopen = function() {
      console.log('connected open!');
    }

    socket.onerror = function() {
      console.log('connection error!');
    }

    socket.onclose = function() {
      console.log('connection closed!');
    }

    socket.onmessage = function(e) {
      document.getElementById('messages').innerHTML += '<p>' + e.data + '</p>';
    }

Первый метод сработает при успешном обмене заголовками, второй – ошибка создания объекта связи или ошибка “рукопожатия”. Следующий – это закрытие соединения и последний – принятие сообщения на сокете.

В отличии от PHP javascript-обьект уже содержит методы и автоматически кодирует/декодирует фреймы.

Заключение

Этой статьей я попытался объяснить технологию вебсокетов максимально просто. Увесь код с этой статьи лежит здесь. Данный протокол обмена сообщениями можно использовать не только для real-time приложений, но и как замена ajax(если нужно экономить трафик). Обьяснение простое – вебсокет обменивается “большим” заголовком только при установлении связи, в свою очередь, ajax сопровождает любой свой запрос+ответ двумя “большими” заголовками.