• Как нормализовать БД до 3 НФ?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Связь продукта и заказа через связующую таблицу не совсем верная, тк в будущем (настоящем) может не быть товаров, заказанных ранее. Потому товары для заказа хранятся в отдельной таблице purchases, причем такая "сущность данных" может быть денормализованней товара.

    В товаре у вас находятся характеристики. Но может быть такое, что их число увеличится сильно (и сильно вероятно), обычно их выносят в таблицу features и связь products_features

    Не увидел у вас предложений/офферов/вариантов товара — конкретные ценовые предложения товаров. Например размеры одного товара или разного одного цвета, у этих "сущностей данных" и будет цена и остаток. В вашем случае StorageID и SupplierID

    Все остальное норм и дробится уже по мере развития и усложнения/роста системы
    Ответ написан
    Комментировать
  • Php! или === null?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    if($error === null){} предпочтительный (иногда он обязательный по стандартам), тк вариант if(!$error){} — это авто-приведение к bool:

    • например 0 приведется к false, в то время как он может быть валидным интовым значением
    • пустой массив — тоже может быть валидным значением, но с !$arr он приведется к false... конечно допускать в программе одновременно и [] и null стоит как можно реже, но такое может произойти


    !$any можно использовать комфортно, когда вы (и программа) уверена, что работает либо с объектом, либо с bool
    Ответ написан
    Комментировать
  • Практическое руководство к написанию тестов?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Так как проект боевой, то придется делать не так как в учебниках:
    - Сначала нужно будет писать приемочные/функциональные тесты (тк юнит-тесты скорее всего будут ни к месту, да и покрывать все кейсы будет долго) на критичные места и те, на которые дотянетесь
    - Новые фичи должны максимально возможно покрыты юнитами, и по пути старые кейсы, которых затрагивают изменения. Там где не возможны юниты -- делайте функциональные
    - Старайтесь делать тесты на места, которые меняются
    - Помогайте тестам: пишите код с declare(strict_types=1), чтобы сам код отчасти себя тестировал и был уверен в аргументах, пишите код проще и надежнее

    Чисто практические аспекты -- ну тут нюансов много, целые учебники

    Отдельно:
    - Возьмите Codeception к примеру как тестовый фулл-стек фреймворк
    - Учитесь, у меня например :) (в личку)
    Ответ написан
    Комментировать
  • Входные и выходные параметры в теле функции - нормально?

    Maksclub
    @Maksclub
    maksfedorov.ru
    идёт обращение сразу напрямую к глобальным переменным и во мне всё протестует.

    Чистота функции
    в функциональном программировании функции стремятся быть чистыми, чистые функции просты, надежны и легко тестируемы.
    Ответ написан
    Комментировать
  • Как работать с несколькими исключениями?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Все зависит от того, являются ли оба класса вместе частью одного слоя или это взаимодействие между разными слоями.

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

    class FirstException extend ModuleException {}
    class SecondException extend ModuleException {}
    
    class ModuleFirst 
    {
         public function work()
         {
              throw new FirstException('first);
         }
    }
    class ModuleSecond 
    {
         public function work()
         {
              throw new SecondException('second);
         }
    }

    тогда дальше ловить из слоя можно так
    try {
        $module->work();
    } catch(ModuleException $e) {
         // обработка
    }


    Если взаимодействие между слоями, то оборачивать каждый раз и выбрасывать свое дальше.
    Пример

    class FirstException {}
    class SecondException {}
    
    class ModuleFirst 
    {
         public function work()
         {
             try {
                $module2->work();
            } catch(SecondException $e) {
                 throw new FirstException($e->getMessage, 0, $e);
            }
         }
    }
    
    class ModuleSecond 
    {
         public function work()
         {
              throw new SecondException('second);
         }
    }


    try {
         $module1->work();
    } catch(FirstException $e) {
        // обработка
    }

    Ответ написан
  • Symfony 4 как написать middleware для меню?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Twig Extension

    Создайте свой экстеншн, передав ему ваши сервисы (репозиторий например)
    В методе getFunctions() зарегайте свою твиг-функцию

    Зарегайте его с тегом twig.extension
    Вызывайте в ваших шаблонах
    Ответ написан
  • Не могу понять почему работает последняя исключения?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Числа с плавающей точкой подчиняются стандарту IEEE-754.

    Если v > 0 и h == 0, то v/h == INFINITY. Гарантировано, что INFINITY > 0.5.
    Если v < 0 и h == 0, то v/h == -INFINITY. Для него -INFINITY < 0.5.
    Если v == 0 и h == 0, то v/h == NaN. Для него любое сравнение, ЕМНИП, возвращает false.

    INFINITY -- бесконечность, у нее тип double/float

    https://en.wikipedia.org/wiki/Division_by_zero#In_...
    Ответ написан
    Комментировать
  • Как осуществить пере адресацию после авторизации на ранее запрашиваемый ресурс?

    Maksclub
    @Maksclub
    maksfedorov.ru
    По дефолту работает так, как вы и хотите:

    By default, the form will redirect to the URL the user requested (i.e. the URL which triggered the login form being shown). For example, if the user requested http://www.example.com/admin/post/18/edit, then after they have successfully logged in, they will be sent back to http://www.example.com/admin/post/18/edit.

    Redirecting after Success¶


    Видимо у вас поведение изменено через always_use_default_target_path:
    Changing the default Page¶

    ...или иным способом...
    Ответ написан
    5 комментариев
  • Цепочка обязанностей и Декораторы, в чем передавать данные?

    Maksclub
    @Maksclub
    maksfedorov.ru
    при анализе код увидел, что постоянно лазию в бд за одной записью (за одной и той же), чего, конечно же хотелось бы избежать. посоветуйте пожалуйста, как передавать данные между объектами?
    Есть такой подход как UnitOfWork, в котором складываются объекта из персистенс слоя (слоя хранения), соответственно когда идет повторное обращение — запрос в БД не идет...

    использую АR.
    А вот тут проблема, тк AR модуль — кусок работы БД, а не отделенная от нее штука. То есть она (модель) ходит в хранилище, а не за ней куда-то и кто-то ходите... Ярчайший пример, когда нарушение SRP очень больно сказывается.

    Решение: можно навернуть сверху модельки некоторый провайдер, который будет персистить загруженные объекты, и по программе путешествовать именно не модель, а провайдер... Тогда можно будет доставать из него имеющий объект, а если нет его — загружать
    Ответ написан
    Комментировать
  • Doctrine как разрешить состояние гонки?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Помимо варианта с Lock: ON CONFLICT (expression) DO NOTHING и похожие решения в других СУБД могут быть иногда довольно подходящими в виду простоты реализации...

    Это выглядит буквально вот так:
    $sql = <<<SQL
    INSERT INTO table (id, value) 
    VALUES (555, \'uniqValue\') 
    ON CONFLICT (value) DO NOTHING
    SQL;
    
    $this->em->getConnection()->executeQuery($sql);


    Если это некие логи или словари с простой логикой хранения, то в самый раз...
    Ответ написан
    Комментировать
  • Как сделать рандом с процентами?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Можно для каждого значения сделать вес, можете сделать так, чтобы сумма все х весов была 100,
    но не обязательно — это могут быть произвольные значения. Главное, что доля веса от суммы всех весов — будет вероятностью данного элемента.

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

    Обновление от 13.06.2020:
    Так вышло, что мой тимлид математик и нашел интересное и эффективное решение этой задачи
    <?php
    
    $values = [
        ['value' => 'One', 'weight' => 20],
        ['value' => 'Two', 'weight' => 30],
        ['value' => 'Three', 'weight' => 50]
    ];
    
    function randByWeight(array $arr) {
        $max = 0;
        $result = [];
        foreach($arr as $value) {
            $rand = pow((mt_rand() / (mt_getrandmax() + 1)), 1/$value['weight']);
            if ($rand > $max) {
                $max = $rand;
                $result = $value;
            }
        } 
    
        return $result;
    }
    
    // Например: 'Three' выпадет в 50% случаев, тк его вес -- половина от суммы всех весов
    // Например: 'Two' выпадет в 30% случаев, тк его вес -- 30% от суммы всех весов
    var_dump(randByWeight($values));

    sandbox.onlinephpfunctions.com/code/39de2d54a298de...

    Чем хорошо это решение — его можно использовать прямо в SQL запросе!
    SELECT * FROM table ORDER BY POWER(random(), 1/weight) DESC LIMIT 1


    Классическое решение:
    <?php
    
    $values = [
        ['value' => 'One', 'weight' => 20],
        ['value' => 'Two', 'weight' => 30],
        ['value' => 'Three', 'weight' => 50]
    ];
    
    function randByWeight(array $items) {
        $sum = array_reduce($items, function(int $acc, array $item): int {
            return $acc += $item['weight'];
        }, 0);
        
        $rand = (mt_rand() / (mt_getrandmax() + 1)) * $sum;
    
        foreach($items as $item) {
            if($rand < $item['weight']) {
                return $item;
            }
            
            $rand -= $item['weight'];
        }
    }
    
    // Например: 'Three' выпадет в 50% случаев, тк его вес -- половина от суммы всех весов
    // Например: 'Two' выпадет в 30% случаев, тк его вес -- 30% от суммы всех весов
    var_dump(randByWeight($values));

    sandbox.onlinephpfunctions.com/code/aee9dca651213e...
    Ответ написан
    3 комментария
  • Как клонировать обьект?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Ваш пример скорее сего в методе copy1(), но вдруг и другие понадобятся:
    <?php
    
    class Any
    {
        private $a;
        
        public function __construct(string $a)
        {
            $this->a = $a;
        }
        
       // Простое клонирование 1 в 1
        public function copy1(): self
        {
            return clone $this;
        }
        
        // Для примера, как сделать похожий объект на основе состояния текущего объекта
        public function copy2(): self
        {
            $new = new self($this->a);
            // тут делаем еще что-либо
            // $new->a тут доступна напрямую
    
            return $new;
        }
        
        // Меняем состояние и возвращаем новый объект, иммутабельное поведение
        // хорошая практика
        public function withNewA(string $a): self
        {
            $new = clone $this;
            $new->a = $a;
    
            return $new;
        }
    }
    
    $any = new Any("A");
    
    var_dump($any === $any->copy1());  // false
    var_dump($any === $any->copy2());  // false
    var_dump($any === $any->withNewA('B'));  // false, изменили объект, но получили новый, а не измененный старый

    sandbox.onlinephpfunctions.com/code/30bb3f1fb842ef...
    Ответ написан
    1 комментарий
  • Вывод вложенного массива из многомерного массива?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    В самом низу написал — зачем такой код писать не нужно...

    Готовый код под вашу задачу:
    <?php
    
    $input = '{"success":true,"data":[{"sport_key":"soccer_korea_kleague1","sport_nice":"K League 1","teams":["Jeonbuk Hyundai Motors","Suwon Samsung Bluewings"],"commence_time":1588932000,"home_team":"Jeonbuk Hyundai Motors","sites":[{"site_key":"unibet","site_nice":"Unibet","last_update":1588281960,"odds":{"h2h":[1.75,3.85,3.7]}},{"site_key":"sport888","site_nice":"888sport","last_update":1588282210,"odds":{"h2h":[1.74,3.8,3.65]}},{"site_key":"onexbet","site_nice":"1xBet","last_update":1588282134,"odds":{"h2h":[1.87,3.96,3.5]}},{"site_key":"betfair","site_nice":"Betfair","last_update":1588281947,"odds":{"h2h":[1.68,1.28,1.25],"h2h_lay":[85.0,85.0,85.0]}},{"site_key":"paddypower","site_nice":"Paddy Power","last_update":1588281989,"odds":{"h2h":[1.67,4.2,3.6]}}],"sites_count":5},{"sport_key":"soccer_korea_kleague1","sport_nice":"K League 1","teams":["Sangju Sangmu FC","Ulsan Hyundai FC"],"commence_time":1589000400,"home_team":"Ulsan Hyundai FC","sites":[{"site_key":"unibet","site_nice":"Unibet","last_update":1588281960,"odds":{"h2h":[3.85,1.88,3.3]}},{"site_key":"sport888","site_nice":"888sport","last_update":1588282210,"odds":{"h2h":[3.8,1.85,3.25]}},{"site_key":"onexbet","site_nice":"1xBet","last_update":1588282134,"odds":{"h2h":[3.76,1.92,3.5]}}],"sites_count":3},{"sport_key":"soccer_korea_kleague1","sport_nice":"K League 1","teams":["Daegu FC","Incheon United"],"commence_time":1589009400,"home_team":"Incheon United","sites":[{"site_key":"unibet","site_nice":"Unibet","last_update":1588281960,"odds":{"h2h":[2.23,2.95,3.2]}},{"site_key":"sport888","site_nice":"888sport","last_update":1588282210,"odds":{"h2h":[2.2,2.9,3.2]}},{"site_key":"onexbet","site_nice":"1xBet","last_update":1588282134,"odds":{"h2h":[2.28,2.98,3.3]}}],"sites_count":3},{"sport_key":"soccer_korea_kleague1","sport_nice":"K League 1","teams":["Sangju Sangmu FC","Seongnam FC"],"commence_time":1589018400,"home_team":"Sangju Sangmu FC","sites":[{"site_key":"unibet","site_nice":"Unibet","last_update":1588281960,"odds":{"h2h":[2.38,2.75,3.2]}},{"site_key":"sport888","site_nice":"888sport","last_update":1588282210,"odds":{"h2h":[2.35,2.7,3.15]}}],"sites_count":2},{"sport_key":"soccer_korea_kleague1","sport_nice":"K League 1","teams":["FC Seoul","Gangwon FC"],"commence_time":1589095800,"home_team":"Gangwon FC","sites":[{"site_key":"unibet","site_nice":"Unibet","last_update":1588281960,"odds":{"h2h":[2.38,2.75,3.2]}},{"site_key":"sport888","site_nice":"888sport","last_update":1588282210,"odds":{"h2h":[2.35,2.7,3.15]}},{"site_key":"onexbet","site_nice":"1xBet","last_update":1588282134,"odds":{"h2h":[2.42,2.84,3.2]}}],"sites_count":3}]}';
    $data = json_decode($input, true);
    
    function grabBookerData(array $data, string $bookerName): array {
        return array_map(function(array $item) use ($bookerName) {
            // Собираем результирующий объект события
            $event             = new \stdClass();
            $event->sport_nice = $item['sport_nice'] ?? null;
            $event->teams      = $item['teams'] ?? [];
        
            // Из данных по разным букмекерам оставляем h2h, который нам нужен
            $bookersData = array_column($item['sites'], null, 'site_key');
            $event->h2h  = $bookersData[$bookerName]['odds']['h2h'] ?? [];
            
            return $event;
        }, $data['data'] ?? []);
    }
    
    var_dump(grabBookerData($data, 'unibet')); // итоговый набор данных

    sandbox.onlinephpfunctions.com/code/6cb94c1070692d...

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

    Сериалайзеры на PHP:
    Fractal (рекомендую)
    Symfony Serializer
    Zend Serializer
    JMS Serializer — его многие ругают за неторопливость и монструозность, но он довольно функциональный и забирает на себя кучу телодвижений, я бы делал с ним. Он становится небольшой проблемой, когда десятки и тысячи операций идут за некоторое непозволительное число серверов/времени.
    Ответ написан
    5 комментариев
  • PHPUNIT: Как возвращать разные значения в методе мока?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    Так точно работает:
    $this->anyMock
        ->expects($this->exactly(4))
        ->method('doSomething')
        ->withConsecutive(...$args)
        ->willReturnOnConsecutiveCalls(...$results)

    Кол-во $args и $results и цифра в $this->exactly(4) одинаково,
    $args - массив массивов


    UPD: Сейчас глянул код, по идее ваш способ абсолютно эквивалентный и должен работать, либо приведенный мной будет ругаться также. переменные $response1 и $response2 точно имплементят ResponseInterface?

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

    Именно поэтому вам expects() скорее всего и подсказала, в чем дело.

    Решения:
    1. Создавать мок именно в методе, не злобоупотреблять setUp()
    2. Контроллировать число вызовов в методе и аргументов для Consecutive Calls, в этом поможет expects($this->exactly(N))
    Ответ написан
    2 комментария
  • Easy admin. Как проверить путь?

    Maksclub
    @Maksclub
    maksfedorov.ru
    EasyAdmin контроллеры довольно простые, вы просто наследуете EasyAdminController и называете свой контроллер так: EntityName + Controller, то есть для User это будет UserController.

    И для ваших контроллеров как и везде в Symfony вы можете использовать свои роли, например так:
    /**
     * @Security("is_granted('ROLE_CAN_EDIT_USER')")
     */
    class UserController extends EasyAdminController
    Подробнее:
    Аннотация Security
    Symfony Uses Voters

    Также ознакомьтесь с главой EasyAdmin: Security and Permissions¶, особенно про item_permission
    Ответ написан
  • Symfony - phpstan, как устранить это замечание?

    Maksclub
    @Maksclub
    maksfedorov.ru
    try {
         // логика
    } catch(\Throwable $e) {
         // тут например логируем
    
         // тут мы 3-м параметром для нового исключения добавили прошлое исключение
         // чтобы не потерять стектрейс изначальной ошибки, мало ли откуда текущий код был вызван
         // о чем и говорит PhpStan
         throw new MyException('msg', 0, $e);
    }
    Ответ написан
    Комментировать
  • Как действительно поможет ООП в реальной программе?

    Maksclub
    @Maksclub
    maksfedorov.ru
    ООП упрощает код, логику и понимание...
    Но только в случае подготовленного человека. Соответственно пока нет осознанности в происходящем и опыта, то ясен пень — будет сложно и не понятно

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

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

    Без ООП — абстракции не будут иметь четких границ и смысл будет в разобранном состоянии собираться из крупиц по коду и данным, но и такой, процедурный подход, имеет преимущества
    Ответ написан
    Комментировать
  • Как заинжектить репозиторий в другом классе?

    Maksclub
    @Maksclub
    maksfedorov.ru
    TelegramController ts = new TelegramController();

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

    Конечно не инжектятся, вы же их в конструктор объекта, который руками создаете, не добавляете!
    Autowiring -- инструмент DI-контейнера, то есть если контроллер создается из контейнера, то он получит все зависимости. В Spring все вращается вокруг контейнера зависимостей, вы в таком случае создаете сервис, указываете зависимости, а уже контейнер собирает ваш сервис.

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

    Если хотите переиспользовать логику:
    Логика должна быть не в другом контроллере, а в некотором сервисе (объекте), который бы вы вызывали из двух (и более) контроллеров. Тогда все бы легко и ПОНЯТНО работало бы и инжектилось.
    Ответ написан
    9 комментариев
  • Как из PHP добавить путь к js в stack?

    Maksclub
    @Maksclub
    maksfedorov.ru
    Трейт, который используется в фасаде VIew, содержит нужный метод:
    https://github.com/illuminate/view/blob/99fa4b3513...

    По итогу из контроллера или другого места добавить в свой тек скрипты можно через:
    \Illuminate\Support\Facades\View::startPush('scripts', script('./path/to/script.js'));


    Только нужно учесть, что нужен stopPush() для корректной работы, там происходит ob_get_clean()
    Ответ написан
    Комментировать
  • Каким образом получить новый массив из текущего?

    Maksclub
    @Maksclub Куратор тега PHP
    maksfedorov.ru
    1. Сгруппировать товары по catID и категории также:
    $products = [];  // ваш массив с товарами
    
    /** 
     *  Группирует любой массив  массивов по любому полю $indexKey в качестве индекса
     */
    function groupBy(array $input, string $indexKey): array
    {
        return array_reduce($input,  function($res, $data) use ($indexKey){
            if (!isset($data[$indexKey])) {
                 return $res;
            }
            
            $res[$data[$indexKey]][] = $data;
                
            return $res;
        }, []);
    }
    
    $groupedProducts = groupBy($products, 'catID');
    // такое работает при условии, что категория с одним id будет иметь последнее название из найденных, 
    // то есть если id один, а название разные, то возможны не ожидаемые результаты
    $categories = array_column($products, 'catName', 'catID');


    2. Потом просто идете в цикле и выводите:
    foreach($groupedProducts as $catId => $catProducts) {
         // тут доступно название категории $categories[$catId] и ее $catId
    
         foreach($catProducts as $product) {
              // тут выводим сами товары $product
         }
    }


    Для PHP 5 нужно [] заменить на array(), предлагаю это сделать самому :)
    Ответ написан
    7 комментариев