програмирование

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

Создадим класс лоадер

class Loader
{
    public function load(array $urls): array
    {
        $results = [];
        foreach ($urls as $url) {
            try {
                $results[] = file_get_contents($url);
                echo 'Success: ' . $url;
            } catch (\Exception $e) {
                echo 'Error: ' . $url;
            }

        }
        return $results;
    }
}

$loader = new Loader();
$urls = [...];
$loader->load($urls);

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

class Loader
{
    public $onSuccess;
    public $onError;

    public function load(array $urls): array
    {
        $results = [];
        foreach ($urls as $url) {
            try {
                $results[] = file_get_contents($url);
                if ($this->onSuccess) {
                    ($this->onSuccess)($url);
                }
            } catch (\Exception $e) {
                if ($this->onError) {
                    ($this->onError)($url, $e->getMessage());
                }
            }

        }
        return $results;
    }
}

$loader = new Loader();
$urls = ['...'];
$loader->load($urls);

$loader->onSuccess = function ($url) {
    echo 'Success: ' . $url . PHP_EOL;
};

$loader->onError = function ($url) {
    echo 'Error: ' . $url . PHP_EOL;
}

И что мы из этого получили. Mы можем уже этот код использовать хоть в консоле, хоть в вебе, хоть в лог.
Вот в логе мы можем использовать сообщения об ошибке которое передано в верху ($url, $e->getMessage())

//for web
$loader = new Loader();

$loader->onSuccess = function ($url) {
    echo '<p>Success: ' . $url . '</p>';
};
$loader->onError = function ($url, $error) {
    echo '<p>Error: ' . $url . '</p>';
};
$loader->load($urls);

//for log
$loader = new Loader();

$loader->onSuccess = function ($url) {
    Log::info('Load ' . $url);
};
$loader->onError = function ($url, $error) {
    Log::error('Load error ' . $error . ' for ' . $url);
};
$loader->load($urls);

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

class Loader
{
    public $listeners = [];

    public function load(array $urls): array
    {
        $results = [];
        foreach ($urls as $url) {
            try {
                $results[] = file_get_contents($url);
                if (!empty($this->listeners['success'])) {                
                    foreach ($this->listeners['success'] as $listener) {
                        $listener($url);
                    }
                }
            } catch (\Exception $e) {
                if (!empty($this->listeners['error'])) {                  
                    foreach ($this->listeners['error'] as $listener) {
                        $listener($url, $e->getMessage());
                    }
                }
            }

        }
        return $results;
    }
}

$loader = new Loader();
$urls = [...];

$loader->listeners = [
    'success' => [
            function ($url) {echo 'Success: ' . $url . PHP_EOL;},
            function ($url) {Log::info('Load ' . $url);},
    ],
    'error' => [
        function ($url) {echo 'Error: ' . $url . PHP_EOL;},
        function ($url, $error) {Log::error('Load error ' . $error . ' for ' . $url);}
    ],
];

$loader->load($urls);

 То есть что мы делали , определили свойства listener как пустой массив, и уже  передаем несколько обработчиков. Если второго обработчика не передавалось $e->getMessage(), то мы могли свободно вынести отделный метод.
 Давайте теперь организуем так чтобы можно было вот эти два года внутри (if ) вынести в отдельный метод.

Строки success и error будет имена событии.

            try {
                $results[] = file_get_contents($url);
                $name = 'success';
                if (!empty($this->listeners[$name])) {
                    foreach ($this->listeners[$name] as $listener) {
                        $listener($url);
                    }
                }
            } catch (\Exception $e) {
                $name = 'error';
                if (!empty($this->listeners[$name])) {
                    foreach ($this->listeners[$name] as $listener) {
                        $listener($url, $e->getMessage());
                    }
                }

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

            try {
                $results[] = file_get_contents($url);
                $name = 'success';
                $data = [
                    'url' => $url,
                ];
                if (!empty($this->listeners[$name])) {
                    foreach ($this->listeners[$name] as $listener) {
                        $listener($data);
                    }
                }
            } catch (\Exception $e) {
                $name = 'error';
                $data = [
                    'url' => $url,
                    'error' => $e->getMessage(),
                ];
                if (!empty($this->listeners[$name])) {
                    foreach ($this->listeners[$name] as $listener) {
                        $listener($data);
                    }
                }
            }

Вынести одинаковые функции  на отдельный метод и запустить событию.

    try {
        $results[] = file_get_contents($url);
        $this->trigger('success', ['url' => $url]);
    } catch (\Exception $e) {
        $this->trigger('error', ['url' => $url, 'error' => $e->getMessage()]);
    }
    public function trigger(string $name, array $data): void
    {
        if (!empty($this->listeners[$name])) {
            foreach ($this->listeners[$name] as $listener) {
                $listener($data);
            }
        }
    }
$loader->listeners = [
    'success' => [
        function ($data) {echo 'Success: ' . $data['url'] . PHP_EOL;},
        function ($data) {Log::info('Load ' . $data['url'] );},
    ],
    'error' => [
        function ($data) {echo 'Error: ' . $data['url'] . PHP_EOL;},
        function ($data) {Log::error('Load error ' . $data['error'] . ' for ' . $data['url']);}
    ],
];

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

class Loader
{
    public const EVENT_SUCCESS = 'success';
    public const EVENT_ERROR = 'error';

    public $listeners = [];

    public function load(array $urls): array
    {
        $results = [];
        foreach ($urls as $url) {
            try {
                $results[] = file_get_contents($url);
                $this->trigger(self::EVENT_SUCCESS, ['url' => $url]);
            } catch (\Exception $e) {
                $this->trigger(self::EVENT_ERROR, ['url' => $url, 'error' => $e->getMessage()]);
            }

        }
        return $results;
    }


    public function trigger(string $name, array $event): void
    {
        if (!empty($this->listeners[$name])) {
            foreach ($this->listeners[$name] as $listener) {
                $listener($event);
            }
        }
    }
}

$loader = new Loader();
$urls = [...];

$loader->listeners = [
    Loader::EVENT_SUCCESS => [
        function ($data) {echo 'Success: ' . $data['url'] . PHP_EOL;},
        function ($data) {Log::info('Load ' . $data['url']);},
    ],
    Loader::EVENT_ERROR => [
        function ($data) {echo 'Error: ' . $data['url'] . PHP_EOL;},
        function ($data) {Log::error('Load error ' . $data['error'] . ' for ' . $data['error']);}
    ],
];

$loader->load($urls);

Получился вполне хороший код. И этот паттерн называется наблюдатель (Observer). 

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

class SuccessEvent
{
    public $url;

    public function __construct($url)
    {
        $this->url = $url;
    }
}
class ErrorEvent
{
    public $url;
    public $error;

    public function __construct($url, $error)
    {
        $this->url = $url;
        $this->error = $error;
    }
}
        try {
            $results[] = file_get_contents($url);
            $this->trigger(self::EVENT_SUCCESS, new SuccessEvent($url));
        } catch (\Exception $e) {
            $this->trigger(self::EVENT_ERROR, new ErrorEvent($url, $e->getMessage()));
        }
$loader->listeners = [
    Loader::EVENT_SUCCESS => [
        function (SuccessEvent $event) {echo 'Success: ' . $event->url . PHP_EOL;},
        function (SuccessEvent $event) {Log::info('Load ' . $event->url);},
    ],
    Loader::EVENT_ERROR => [
        function (ErrorEvent $event) {echo 'Error: ' . $event->url . PHP_EOL;},
        function (ErrorEvent $event) {Log::error('Load error ' . $event->error . ' for ' . $event->url);}
    ],
];

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

class Loader
{
    public const EVENT_SUCCESS = 'success';
    public const EVENT_ERROR = 'error';

    private $listeners = [];

    public function on($name, callable $listener): void
    {
        $this->listeners[$name][] = $listener;
    }

   ....
}
$loader = new Loader();

$loader->on(Loader::EVENT_SUCCESS,function (SuccessEvent $event) {});
$loader->on(Loader::EVENT_SUCCESS,function (SuccessEvent $event) {});
$loader->on(Loader::EVENT_ERROR,function (ErrorEvent $event) {});
$loader->on(Loader::EVENT_ERROR,function (ErrorEvent $event) {});

$loader->load([...]);

и так мы можем создать другие классы, и как добавим другой класс например  Parser, то уже обе классы будут иметь одинаковые методы, свойства.
Можно это выносить отдельный траит,добавить некий синтаксический сахар. Но мы будем делать другим путем.
Создадим класс EventDispatcher и перенесем , то что нас надо. 
 

class EventDispatcher
{
    private $listeners = [];

    public function on($name, callable $listener): void
    {
        $this->listeners[$name][] = $listener;
    }

    public function dispatch(string $name, array $event): void
    {
        if (!empty($this->listeners[$name])) {
            foreach ($this->listeners[$name] as $listener) {
                $listener($event);
            }
        }
    }
}
class Loader
{
    public const EVENT_SUCCESS = 'loader.success';
    public const EVENT_ERROR = 'loader.error';

    private $dispatcher;

    public function __construct(EventDispatcher $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }

    public function load(array $urls): array
    {
        $results = [];
        foreach ($urls as $url) {
            try {
                $results[] = file_get_contents($url);
                $this->dispatcher->dispatch(self::EVENT_SUCCESS, new SuccessEvent($url));
            } catch (\Exception $e) {
                $this->dispatcher->dispatch(self::EVENT_ERROR, new ErrorEvent($url, $e->getMessage()));
            }

        }
        return $results;
    }
}
class Parser
{
    public const EVENT_SUCCESS = 'parser.success';
    public const EVENT_ERROR = 'parser.error';

    private $dispatcher;

    public function __construct(EventDispatcher $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }

    public function load(array $urls): array
    {
        $results = [];
        foreach ($urls as $url) {
            try {
                $results[] = file_get_contents($url);
                $this->dispatcher->dispatch(self::EVENT_SUCCESS, new SuccessEvent($url));
            } catch (\Exception $e) {
                $this->dispatcher->dispatch(self::EVENT_ERROR, new ErrorEvent($url, $e->getMessage()));
            }

        }
        return $results;
    }
}
$dispatcher = new EventDispatcher();

$dispatcher->on(Loader::EVENT_SUCCESS, function (SuccessEvent $event) {});
$dispatcher->on(Loader::EVENT_SUCCESS, function (SuccessEvent $event) {});
$dispatcher->on(Loader::EVENT_ERROR, function (SuccessEvent $event) {});

$dispatcher->on(Parser::EVENT_SUCCESS, function (ErrorEvent $event) {});
$dispatcher->on(Parser::EVENT_ERROR, function (ErrorEvent $event) {});

$loader = new Loader($dispatcher);
$parser = new Parser($dispatcher);

$loader->load([...]);
$parser->load([...]);