Как стать автором
Обновить

Комментарии 100

Такие штуки было модно писать в конце прошлого тысячелетия. Странно, что вы не нашли ни одной готовой, а написали свою.

Я просто оставлю это здесь:
php.net/manual/ru/pdo.prepared-statements.php
ru.wikipedia.org/wiki/ORM
Странно, что вы не нашли ни одной готовой

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

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

з.ы. сорри, показалось, что вы автор. пропускайте коммент.
Велосипедостроение это хорошо для практики. Но почитайте про ActiveRecord, ORM, Query Builder. Реализаций этого добра много, есть из чего выбрать.
Про ActiveRecord я бы даже не советовал читать, разве что в теме «Как делать не надо».
Нет, почитать стоит. Особенно в варианте PoEAA фаулера (вместе с другим подходами, разбором их плюсов и минусов и т.д.)
Чтобы начать работать, нужно просто, скачав файл, прописать такую строчку
composer

А если посмотреть сюда, то можно увидеть список готовых велосипедов, все же 2018.
НЛО прилетело и опубликовало эту надпись здесь
Отличная подборка велосипедов! thx!

Есть такая хорошая штука как (GitHub/Gitlab/Bitbucket/etc) и посмотрите пожалуйста ее, там всё это "добро" уже есть !

Метод setLimit() работает только на один запрос, после этого лимиты обнуляются.

$DB->setLimit (10);
// т.е. если кто-то добавит новый код, то все может поломаться и у запроса ниже уже не будет LIMIT?
$res = $DB->fetchArray ('clients', "NAME = 'John'");

Кто-то еще качает файлы по ссылке?
Если уж хотите, чтобы люди ознакомились с вашим кодом, залейте на гитхаб

Java-скриптам
Это про JavaScript или про Java приложения?
Зачем «это» пропустили из песочницы… Я все понимаю, у всех разный уровень знания языка, но это ведь явный пример того, как не следует делать в 2018ом году. Просьба — верните «это» обратно в песочницу.
Честно говоря, уже в 2010м так делать было не принято. То, что в статье показано — это уровень 90х. Хотелось бы код увидеть, но, судя по всему, автор не очень понимает как работает сервер и скачать исходник его обертки нам не светит
Кстати, в 2010 году Doctrine исполнилось 4 года, а Propel пять.
НЛО прилетело и опубликовало эту надпись здесь
ActiveRecord это и есть маппинг )
НЛО прилетело и опубликовало эту надпись здесь

вы разные вещи смешиваете. QueryBuilder — это построитель запросов. К объектам отношения не имеет.

Точнее к маппингу записей базы на объекты и обратно отношения не имеет, если говорить о SQL QueryBuilder. Реализуют его обычно всё-таки на объектах, а не функциях :)

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

НЛО прилетело и опубликовало эту надпись здесь
конечному пользователю насрать

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

НЛО прилетело и опубликовало эту надпись здесь

какой-то вы неприятный собеседник

раньше понятия QueryBuilder и ActiveRecord отличались от нынешних

Нет, раньше query builder как подход и active record как паттерн были тем же чем они являются сегодня. Это как бы суть названий и терминов — если они часто меняются, и у них нет четкого определения — то надо придумать новый термин с четким определением и перестать юзать что-то что вводит в заблуждение людей.


Причина по которой под ActiveRecord понимают все что угодно произрастает из безграмотности людей и подмены понятий (когда "паттерн" какой-то или идею подменяют конкретной библиотекой).


и люди писали самодельные велосипеды на файлах.

в 70-х? да, было такое. только тогда небыло ни mysql ни php да и концеп SQL-я только только зараждался. И нет ничего плохого в хранении данных в файлах, просто это сегодня имеет смысл только если вам надо специализированное хранилище (и вряд-ли вы что-то такое будете на php писать, хотя можно).

А о том, что со временем и сами названия меняются — тем более.

от того что вы юзаете code igniter термин query builder своего значения не поменял ни разу. Это штука которая позволяет вам строить запрос. причем за выполнение этого запроса или за работу с result set оно уже не отвечает.


Причем, для того что бы вы могли с result set работать вам нужна какая-то информация о запросе (интроспекция) и проще всего это сделать добавив свой строитель запросов который будет эту инфу предоставлять для других компонентов вроде мэпперов каких и т.д. Ну и что бы не писать что-то типа $db->execute($query) и т.д. можно скрестить это дело и с коннекшен менеджером.


Но это просто конкретное решение конкретной библиотеки и никакого отношения к терминологии все это не имеет.


Что до "конечного пользователя" — проблема штук типа CI ActiveRecord в том что это создает ограничения применимости решений. Иногда хочется и не через PDO поработать а через какой-то асинхронный драйвер на модных нынче свуле, и оказывается что 99% всех квери билдеров это подкопирку содранные друг к друга куски гуано которые так просто не реюзать.

Авторы CodeIgniter назвали, судя по вашей ссылке, `CodeIgniter ActiveRecord` свою имплементацию паттерна QueryBuilder
Собственно в первом комментарии всё правильно написали. Я бы добавил еще composer и git.
Жаль не получается посмотреть, что там в коде.
Про бесполезную для сообщества работу уже написали, но раз уж Вы сами упомянули, что Вы начинающий программист, то позвольте небольшой Code Review:
Не пишите на PHP как на Python
$res = $DB->fetchArray ('clients'); // получаем все записи в виде массива ассоциативных массивов
if ($res)
    foreach ($res as $client)
        echo print_r ($client, true); // выдаем все записи на экран


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

echo print_r ($client, true);

Это вообще вогнало в некий ступор. Зачем???

if ($res)

Что это? Resource или Result? Не надо сокращать даже такие, казалось бы, элементарные вещи. Ни к чему хорошему это не приведет. Сначала сокращаете error на e, а потом путаетесь между event и error, не говоря уже о переменных типа AvgPrc и прочей ереси, которая вырастает из безобидных казалось бы сокращений.

$res = $DB->fetchArray ('clients', array ("NAME" = "John", ">=AGE" => 25), array ("NAME", "AGE"), array ("ID" => "ASC", "NAME" => "DESC"));

Прочитайте про Fluent Interface
Еще забыл

$res = $DB->fetchArray ('clients'); // получаем все записи в виде массива ассоциативных массивов
echo print_r ($client, true); // выдаем все записи на экран

Вы правда считаете, что подобное комментирование полезно?
«event и error» а как же catch (\Exception $e)?
аналогично, всегда бешусь, когда IDE автодополняет catch с названием переменной $e и ленюсь это поправить
А вообще часто эвенты в catch приходят?)
Если строить велосипед а-ля sentry, то вполне может быть :)
Это отвратительно(
Тут вопрос скорее в сокращении имен переменных, вместо $someError, $someEvent, $someException — везде могут написать $e, и разбирайся потом как хочешь.

ну у вас еще тип есть, который неплохо дополняет какое-то общее $error или $event. Опять же в этом случае нет смысла в суффиксах для типов (InvalidArgumentException вместо InvalidArgument).

А бывает и суффиксов нет, и имя $e :(
Понятное дело, что IDE и знание проекта всегда помогут, всегда подскажут. Но если говорить о PHP, это часто подразумевает fullstack разработку, то есть периодически нужно и на JS переключаться, и с базой уметь работать. Так вот из-за того, что люди привыкают сокращать такие вещи или дают однотипные имена, получается неразбериха в том же JS, типа
passwordInput.addEventListener('click', function(event) {
  var minClickAreaPercent = 0.8; // далее 80% от левого края срабатывает переключение
  var toggleIsAble = (event.offsetX / this.clientWidth > minClickAreaPercent);
// какой-то код
}

В котором семантически правильней назвать аргумент функции не event (и уж тем более не e), а например mouseClick или mouseClickEvent, но да, людям лень потратить на 1\2 секунды больше на набор текста. К счастью, подобного рода проблемы не распространяются на долгоживущие проекты с постоянной командой разработки.
Кмк, если есть проблемы с идентификацией аргумента catch, то проблема вовсе не в этом. Я неместный в PHP, но события (если речь о «наблюдателе») через исключения кажутся крайне сомнительным решением. А сам язык позволяет указывать тип исключения. В отличие от функций, у catch едва ли можно подчеркнуть какую-либо семантику, которую не сможет выразить тип.
У меня лично проблем с идентификацией исключений нет, но именовать так переменные — не самый лучший вариант. Никто не запрещает использовать $e для ошибок, ивентов или исключений, но $someEvent будет гораздо понятнее, чем $e. Изначально у автора $res — resource или result? Предполагаем, что таки result, но автор мог и до $r тогда сократить, а тут уже гораздо больше вариантов интерпретации имени переменной другими разработчиками (как минимум row можно ещё предположить).
Я и сам раньше грешил переменными типа $fido, $fids — мне понятно, что $fido — ресурс открытый fopen на чтение, fids — на запись, но потом сам начинаешь в этом путаться, переназначать случайно эти переменные.
У меня лично проблем с идентификацией исключений нет, но именовать так переменные — не самый лучший вариант.

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

Тянуть сюда аналогию с локальными переменными или формальными параметрами функций — можно, но, имхо, это уже педантизм)
Может быть и педантизм, но есть стандарты (PSR для php) и общепринятые практики, их было бы неплохо соблюдать, меньше придирок будет.
Ну вот PSR как раз рекомендует $e.

К любым гайдам и стандартам нужно подходить критически, не отключать мозг, включая линтеры и автоформатеры. Некоторым людям порой свойственно изображать педанта и часами спорить о малозначимой ерунде. Это точно хуже, чем просто плохой стиль)
PSR — это принцип форматирования кода, а не принцип семантического именования. PSR ругнется на название класса с маленькой буквы, но ему без разницы если класс называется Ee. В приведенном примере говорится лишь об отступах, скобках и порядке
Буду занудой, но PSR — это PHP Standard Recommendation. PSR-1 и PSR-2 действительно являются рекомендациями к форматированию и стилю кода, однако есть, к примеру, PSR-3, PSR-6 и PSR-7, которые описывают интерфейсы для логирования, кеширования и взаимодействия посредством HTTP-протокола соответственно.
Но они по факту тоже не объясняют и, тем более, не диктуют принципы именования. Они дают жёстко заданные имена, которые нужно использовать в конкретных кейсах для совместимости, но не задают даже имён наследников.
Зачем же вы тогда сослались на него?) Ладно, предположим, что это недостаток конкретного документа (хотя судя по стайлгайдам других языков с нормальными примерами, никто еще не придумал внедрять семантику в имя переменной-исключения).

Хорошо, давайте исследовать «общепринятые практики» из живых проектов на PHP: YII, Composer, WordPress, Joomla и куча встреченных мной топ-проектов на GitHub (по звездам) используют "$e" или что-то не менее «семантичное», вроде "ex" или "err". Выходит, это авторское изобретение.
Я не ссылался на PSR. А проблема заключается имхо в другом:
Выходит очередная статья, документация, книга, любая авторская работа. Если она становится популярна, как с точки размера аудитории, так и с точки зрения вклада в сообщество, то многие принимают ее за чистую монету и считают, что абсолютно все в этому труде нужно брать за эталон => в том числе берут оттуда такую информацию, которую автор возможно и не планировал донести.

Это явно прослеживается и в примере с try catch из PSR. Суть этого правила заключается в отступах, порядке и скобках, а саму переменную исключения добавили потому что без нее пример казался бы некорректным. Разумеется ее нельзя выкидывать даже для примера, поэтому по привычке добавили $e.

Именно по привычке, потому что еще раньше эту несчастную $e опубликовали в какой-нибудь известной статье, а автор этой статьи еще раньше прочитал известную книгу, где автор повествовал об обработке исключений, но решил сэкономить на печатном пространстве и сократил $exception до $e, ведь в этой книге вообще не было целей говорить о семантике и правилах хорошего тона в коде.
Да, я не обратил внимания на то, как вы внедрились)

Это достаточно очевидное сокращение в данной синтаксической конструкции, чтобы для него воскрешать венгерскую нотацию. Еще и стандарт де-факто. Допустимая вкусовщина, а не хороший тон, кмк.
Ну вот PSR как раз рекомендует $e.

нет, он рекомендует расстановку пробелов и скобочек. То, что там написано $e, к стандарту не относится.

\Exception все-таки чуть ближе к error, чем к event, поэтому такой вариант еще приемлим)
Действительно, тема вечная: давно есть адекватные решения, но новички продолжают писать своих убийц pdo с sql injection и выборками в коде, но зато без *фатального недостатка*
Ну это же хорошо. На то они и новички. Когда новичек, писавший велосипед, возьмет в руки настоящий инструмент, он сразу поймет его, прочувствует идеи. Усредненный новичек, который не писал велосипед, потратит на освоение инструмента больше времени, и, возможно, вообще никогда не поймет так же хорошо, как «велосипедист».
Получился бы отличный пост на первое апреля.
Большинство php-ных строителей запросов чуть-чуть лучше чем тупая конкатенация строк. Увы. Неужто людям больше от жизни не надо?
видать задачи где подойдет именно «строитель запросов чуть-чуть лучше чем тупая конкатенация строк» простые и сводятся к обычным выборкам максимум с парой условий. Или же людям, которые занимаются в основном такими задачами, лень потыкать существующие отлаженные инструменты и проще сделать очередной костыль тупо под задачу и использовать его
Если именно строители запросов, то что там особо больше ждать? Другое дело, если используются высокоуровневые абстракции типа ORM, Repository и Criteria.
Где-то месяца 3-4 назад я тоже так думал а потом начал загоняться по всяким там возможностям выводить типы, верифицировать запрос (даже по частям) на соответствие схеме без необходимости запуска оного, вопросы композиции запросов (открыл для себя парочку вариантов на тупой конкатенации которые интереснее и удобнее чем то что предлагает большинство билдеров, через под запросы естественно). Ну и т.д.

Что до абстракций типа ORM, репозиториев и критерий — все это прекрасно работает опять же в ситуации когда у тебя все выборки можно влипить в WHERE. без джойнов, подзапросов и прочих оконных функций. Чуть сложнее и абстракции эти перестают помогать (в том виде в котором они существуют в PHP) что опять же ставит вопрос «а нафига оно все надо!?» Ведь взять ту же доктрину (как бы я ее не любил) но на ее изучение нужно немало времени, в ней полно багов, это очень сложный продукт сам по себе… и тот факт что оно помогает мне только для очень простых задач меня очень расстраивает.
а потом начал загоняться по всяким там возможностям выводить типы, верифицировать запрос (даже по частям) на соответствие схеме без необходимости запуска оного

Блин, а зачем так упарываться?

Я вот объективно не видел систем такой сложности где это бы требовалось и было в хоть сколько-то окупаемом виде.
> где это бы требовалось

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

Да и опять же. имея интроспекцию можно много тупого кода удалить.
Мысль в другом. Это «дорого» по трудозатратам в целом на единицу проблемы.
Расскажите мне как вы проводили эстимацию затрат и масштабов проблем?
Методом сделали — оценили.

Устранение проблем с ошибками схемы в среднем занимало 1-2 инциндента по 0.1-0.5 часа на 4 двухнедельных релиза.
Т.е. за год работы не более 6.5 часов

Парсинг и анализ чистых SQL из кода больше трех дней занял у разработчика насколько я помню.
Анализ запросов формируемых QueryBuilder и DQL в общем случае не взлетел и решили не продолжать после чистого SQL.

Всем спасибо за комментарии. Исходник выложен на GitHub, ссылка в конце статьи
Это примитивная библиотека, которая просто упрощает взаимодействие с MySQL.
Все, о чем пишут многие — это уже ближе к ActiveRecord.
Свое видение по ActiveRecord я опубликую отдельно, в виде еще одного «велосипеда».
Для создания этой библиотеки я использую ту, про которую написана это статья.
Третий «велосипед» — это связь между серверной ActiveRecord и фронтовой JavaScript. То есть попытка сделать ActiveRecord для JavaScript с заранее сконфигурированной базой данных на сервере.
«Велосипеды» — может это и плохо, особенно в 2018 году, когда пакетов, библиотек, фреймворков сделано уже очень много. Но — это же форум разработчиков. И каждый раз изобретая очередной «велосипед» может родиться умная и конструктивная дискуссия. А отчасти ради этого и существует данный проект.
Всем удачи!
> И каждый раз изобретая очередной «велосипед» может родиться умная и конструктивная дискуссия.

А толку? Ну то есть… смотрите. Вы написали велосипед. Причем не просто велосипед, а примерно такой же как пишут его сотни других разработчиков ежегодно (если не еженедельно). На хабре подобные велосипеды постят раз в пол года примерно (вот типа одинаковые по работе с базой), и будут продолжать постить. «Форум разработчиков» же.

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

А потому — толку для сообщества нет от слова совсем.
Хабр — не форум разработчиков.
Вам настоятельно рекомендую разобраться с PSR и PHPDoc, то что вы выложили на гитхаб — страх и ужас. Методы то паблик, то протектед, то без указания области видимости (паблик то бишь), фигурные скобки то есть, то нету, комментарии в таком стиле — ужасны, хотя я смотрю вам уже пулреквест выкатили с phpdoc-комментариями.
Пока что ваш велосипед вызывает глазное кровотечение и физическую боль.
Перечитайте внимательнее ваш код, я конечно потратил 10 минут на пуллреквест с некоторыми правками, но там работы над этим велосипедом непочатый край.
вы имеете ввиду вопросы оформительского характера или что-то более серьезное?
если второе — то дайте пару наводок

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


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

«Заворачивать» один оператор в класс? Никогда не понимал этого. Тем более, запрос к БД. Это очень гибкий язык и заранее себя ограничивать…
Добро бы в классе была бы структура базы и класс организовывал бы левые соединения.
Доктрина, Эктиврекорд, много их. И многие умеют работать из коробки более чем с одной СУБД, позволяют описывать структуру БД, строить сложные запросы, для результата реализовывать инклуды и прочие радости. Только всё это хорошо работает на небольших, не сильно нагруженных проектах, потому как иногда получаются неоптимизированные sql-запросы, для результата создаётся вагон объектов, что в итоге дорого по памяти и нагрузке на cpu. PDO и чистый sql всё ж иногда выигрышнее. Но нужно sql знать и понимать все риски такого подхода.
«Поскольку у меня скрипты на PHP все больше и больше начинают сворачиваться к одной задаче — выборке из базы данных и передаче этих данных клиентским Java-скриптам...»

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

PS После многих лет работы с 1С в обычных SQL-ных запросах не хватает обращения к полям связанной таблицы через точку…

То есть если у нас есть таблица sales, в котором есть поле customer, содержащие id записи таблицы customers, то для его получения можно было написать запрос вида

SELECT sales.date, sales.amount, sales.customer.name as name
FROM sales

То есть получить поле связанной таблицы без прописывания этой таблицы через JOIN в запросе. Отчасти в PHP-фреймворках это решается через модели и метод «WITH», но не так удобно. Было бы здорово увидеть реализацию подобной возможности для PHP.

SELECT s.date, s.amount, c.name FROM Sales s JOIN s.customer c — пожалуй, самое близкое из более менее известных

Было бы здорово увидеть реализацию подобной возможности для PHP.

Doctrine и DQL.


select sales.date, sales.amount, customer.name 
FROM App\Sales sales JOIN sales.customer customer

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

Doctrine и DQL.


Это почти то, но не совсем, так как в 1С это реализовано без Join, то есть если customer является id записи другой таблицы, то обращаться к customer.name можно без строчки
JOIN sales.customer customer


но в целом я бы не назвал это прям таким удобным (потому что в большинстве случаев я бы хотел видеть в объектой модели айдишки а не целые сущности)


А зачем вам id само по себе? Что вы с ним будете делать?
В любом случае тут могут быть два подхода.
1) в случае указания в запросе customer без свойств возвращать id а не целиком модель.
2) В запросе написать customer.id

> А зачем вам id само по себе? Что вы с ним будете делать?

Отдать клиенту, например. А уж если ему нужны детали, то он сделает запрос на их получение по id.

1) связь может быть не по полю id, и даже не по primary key
2) тут построитель должен быть достаточно умным, чтоб не джойнить customer, а отдать ссылку на него. А если такой записи в customer нет? ну и 1 тоже справедливо
Отдать клиенту, например. А уж если ему нужны детали, то он сделает запрос на их получение по id.


Отдать клиенту вы можете customer.id и это будет даже более грамотно в случае если такой записи нет. Тогда customer.id возвратит null и у клиента не будет иллюзии, что такая запись существует

1) связь может быть не по полю id, и даже не по primary key
2) тут построитель должен быть достаточно умным, чтоб не джойнить customer, а отдать ссылку на него. А если такой записи в customer нет? ну и 1 тоже справедливо


1) Речь идет именно о связи с таблицей сущностей с уникальным primaryKey, имя которого указано в foreignKey(использование иных имен кроме id мне кажется странным и непрофессиональным, но не суть)
2) Это очень простое условие, перебираются поля, если есть обращение через точку, то добавляется join. Если такой записи нет, то как сказано выше возвращается null для всех полей customer
и это будет даже более грамотно в случае если такой записи нет.

Очень сильно зависит от нюансов. Например, записи нет, а дело в архиве (бумажном) есть.


Речь идет именно о связи с таблицей сущностей с уникальным primaryKey, имя которого указано в foreignKey(использование иных имен кроме id мне кажется странным и непрофессиональным, но не суть)

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


Это очень простое условие, перебираются поля, если есть обращение через точку, то добавляется join. Если такой записи нет, то как сказано выше возвращается null для всех полей customer

То есть не просто JOIN даже, а LEFT JOIN? А если задача вывести как раз отчёт по битым ссылкам?

То есть не просто JOIN даже, а LEFT JOIN? А если задача вывести как раз отчёт по битым ссылкам?


WHERE ISNULL(customer.id)


А если где-то валидный null?
В первичном ключе?
В ссылке. Где-то валидные sales.customer_id, где-то битые, а где-то валидные null. И нам нужны только битые.
Само поле customer возвращает свое значение, соответственно
customer не NULL, а customer.id NULL — это и будет битой ссылкой
А зачем вам id само по себе? Что вы с ним будете делать?

хранить, на чтение у меня другие штуки. ORM на чтение не нужна.

В доктрине это все есть


$sale = $em->find('Sale', 1);
echo "Customer: " . $sale->getCustomer()->getName() . "\n";
Ну в таком ключе «это» есть в любой ORM которая умеет в lazy + eager загрузку данных.

Об этом и речь. Банальный lazy load преподносится в 1С как космическая технология.

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

Мои соболезнования.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории