Php ООП. Переключение на другой сайт при недоступности первого

Обсуждаем, как правильно строить приложения
Ответить
Vladbara705
Сообщения: 10
Зарегистрирован: 2019.02.23, 22:55

Php ООП. Переключение на другой сайт при недоступности первого

Сообщение Vladbara705 » 2019.03.01, 15:23

Имеется задача, нужно посетить сайт и спарсить информацию. Если сайт недоступен, то переключиться на другой и спарсить с него. Как реализовать это на ООП с соблюдением solid?
Предполагается, что список сайтов можно расширить.

Обязательно ли при объявлении списка сайтов руководствоваться принципом закрытости,открытости?
Если можно им руководствоваться, то как получить массив с урл этих сайтов?

Прошу нацелить на правильный путь.

Аватара пользователя
samdark
Администратор
Сообщения: 9134
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Php ООП. Переключение на другой сайт при недоступности первого

Сообщение samdark » 2019.03.01, 17:41

Давайте с чего-то начнём. Какие классы вы планируете сделать с какими интерфейсами?

Vladbara705
Сообщения: 10
Зарегистрирован: 2019.02.23, 22:55

Re: Php ООП. Переключение на другой сайт при недоступности первого

Сообщение Vladbara705 » 2019.03.02, 02:24

Сделал так, прошу оценить по всем критериям. Тестируемость, solid, комментирование и т.д.

Код: Выделить всё

<?php

namespace app\models;

use Yii;
use yii\base\Model;
use yii\base\ErrorException;

    class Euro extends Model
    {
        protected $receiveBody;
        protected $OutHtml;

        public function Euro()
        {
            $receiveBody = new Body();
            $OutHtml = new OutHtml();
            $parseXML = new parseXML();
            $parseJSON = new parseJSON();
            $receiveEuro = new receiveEuro($receiveBody, $OutHtml, $parseXML, $parseJSON);
            return $receiveEuro->Euro();
        }
    }

/*#######################################################################
Класс, в который было все инкапсулировано.
Здесь заданы УРЛ сайтов, с которых берется информация.
Для каждой страницы указывается формат данных и ссылка на страницу.
При вызове определенной страницы берется ее формат и в зависимости от этого вызывается определенный класс и его метод.

P.S если изменить урл ссылки - страница станет недоступна и будет загружаться следующая страница
*/#######################################################################
   class receiveEuro
   {
        protected $receiveBody;
        protected $outhtml;
        protected $parsexml;
        protected $parsejson;
        private $url;
        private $body;

        public function __construct(Body $receiveBody, OutputInterface $outhtml, ParseInterface $parsexml, ParseInterface $parsejson)
        {
            $this->receiveBody = $receiveBody;
            $this->outhtml = $outhtml;
            $this->parsexml = $parsexml;
            $this->parsejson = $parsejson;
        }


        public function Euro()
        {
            $massURL = array ('xml' => 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml',
                              'json' => 'https://www.cbr-xml-daily.ru/daily_json.js');
            foreach ($massURL as $key => $value)  //проходим циклом по списку ссылок в массиве massURL
            {
                $body = $this->receiveBody->receiveBody($value); //пытаемся получить тело страницы
                if ($body)  // если страница загрузилась и исключения нет - то идем далее
                {
                    switch ($key)  // здесь смотрим формат страницы и вызываем необходимый класс-метод
                    {
                        case 'xml':
                            $result = $this->parsexml->parsing($body);
                            break;
                        case 'json':
                            $result = $this->parsejson->parsing($body);
                            break;
                    }
                    return $this->outhtml->out($result,$value);
                    break; // если первая же страница загрузилась, то выходим из цикла
                }
            }
            
        }
   }

/*#######################################################################
Класс, который отвечает за получение тела страницы и проверку на доступность сайта.
В случае, если сайт не доступен обрабатывается исключение и возвращается false
*/#######################################################################
   class Body
   {
        private $url;

        public function receiveBody($url)
        {
            try {
                    return file_get_contents($url);
                }
                catch (ErrorException $e)
                {
                    return false;
                }
        }
   }

/*#######################################################################
Класс, который отвечает за XML парсинг
*/#######################################################################
    interface ParseInterface
    {
        public function Parsing($body);
    }

   class parseXML implements ParseInterface
   {
        private $body;
        private $result;
        private $json;

        public function Parsing($body)
        {
            $xml = simplexml_load_string($body);
            $json = json_encode($xml->children());
            preg_match('#"currency":"RUB","rate":"(.*?)"}#i',$json,$result);
            return $result[1];
        }
   }

/*#######################################################################
Класс, который отвечает за JSON парсинг
*/#######################################################################
   class parseJSON implements ParseInterface
   {
        private $body;

        public function Parsing($body)
        {
            $json = json_decode($body,true);
            return ($json["Valute"]["EUR"]["Value"]);
        }
   }

/*#######################################################################
Класс, который отвечает за вывод информации. 
Реализовал через интерфейс на случай, если понадобится вывод в другом виде
*/#######################################################################
    interface OutputInterface
    {
        public function out($result,$url);
    }

   class OutHtml implements OutputInterface
   {
        private $result;
        private $url;

        public function out($result,$url)
        {
            return 'Курс доллара равен <strong>'.$result.'</strong> по данным <strong>'.$url.'</strong>';
        }
   }

anton_z
Сообщения: 419
Зарегистрирован: 2017.01.15, 15:01

Re: Php ООП. Переключение на другой сайт при недоступности первого

Сообщение anton_z » 2019.03.02, 06:57

1. По мне ParseInterface и его наследники абсолютно лишние, форматы для сайтов строго заданы, какая тут может быть замена со временем?
2. Класс Body тоже по-моему лишний. Для него то отдельная абстракция вообще зачем?
3. По PSR2 код бы форматировать.

Короче, я бы сделал так:

Код: Выделить всё



<?php

namespace currency;

use Respect\Validation\Validator as v;

class Url {

    /**
     * @var string
     */
    private $url;

    public function __construct(string $url)
    {
        if (!v::url()->validate($url)) {
            throw new \InvalidArgumentException('Некорректный url.');
        }
        $this->url = $url;
    }

    public function toString() : string
    {
        return $this->url;
    }

}

class Rate {


    /**
     * @var string
     */
    private $rate;


    public function __construct(string $rate)
    {
        //с регуляркой мог налажать, лень проверять
        \Webmozart\Assert\Assert::regex($rate, '/^\d+\.\d{2}$/');
        $this->rate = $rate;
    }


    public function toString() : string
    {
        return $this->rate;
    }

}

interface CurrencySource
{

    /**
     * @return Rate
     * @throws SourceNotAvailableException
     */
    public function euro(): Rate;

    public function url(): Url;

}


class SourceNotAvailableException extends \RuntimeException
{

}

class CBRFCurrencySource implements CurrencySource
{


    /**
     * @var Url
     */
    private $url;

    /**
     * CBRFEuroSource constructor.
     *
     * @param $url
     */
    public function __construct(Url $url = null)
    {
        if ($url === null) {
            $url = new Url('https://www.cbr-xml-daily.ru/daily_json.js');
        }
        $this->url = $url;
    }


    public function euro(): Rate
    {

        $response = file_get_contents($this->url->toString());

        if ($response === false) {
            throw new SourceNotAvailableException();
        }

        $json = json_decode($response, true);

        if (json_last_error() != JSON_ERROR_NONE) {
            throw new LogicException('Некорректный формат ответа сервиса.');
        }

        return new Rate($json["Valute"]["EUR"]["Value"]);

    }


    public function url() : Url
    {
        return $this->url;
    }


}


class ECBCurrencySource implements CurrencySource
{



    /**
     * @var Url
     */
    private $url;

    /**
     * CBRFEuroSource constructor.
     *
     * @param $url
     */
    public function __construct(Url $url = null)
    {
        if ($url === null) {
            $url = new Url('http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml');
        }
        $this->url = $url;
    }



    public function euro() : Rate
    {

        $response = file_get_contents($this->url->toString());

        if ($response === false) {
            throw new SourceNotAvailableException();
        }

        $xml = simplexml_load_string($response);

        if ($xml === false) {
            throw new \LogicException('Некорректный формат ответа сервиса.');
        }

        //поему бы прямо из xml знчение не считать? зачем кодировать в json?

        $json = json_encode($xml->children());

        if (json_last_error() != JSON_ERROR_NONE) {
            throw new \LogicException('Некорректный формат ответа сервиса.');
        }

        preg_match('#"currency":"RUB","rate":"(.*?)"}#i', $json, $result);

        return new Rate($result[1]);

    }

    public function url() : Url
    {
        return $this->url;
    }

}


class CachedCurrencySource implements CurrencySource
{

    /**
     * @var CurrencySource
     */
    private $source;

    /**
     * @var Rate|null
     */
    private $euro;

    /**
     * CachedCurrencySource constructor.
     *
     * @param CurrencySource $source
     */
    public function __construct(CurrencySource $source)
    {
        $this->source = $source;
    }


    public function euro() : Rate
    {
        if ($this->euro === null) {
            $this->euro = $this->source->euro();
        }

        return $this->euro;
    }


    public function url() : Url
    {
        return $this->source->url();
    }


}


class SuperDooperHighAvailableCurrencySource implements CurrencySource
{

    /**
     * @var CurrencySource[]
     */
    private $sources;


    /**
     * @var CachedCurrencySource
     */
    private $successful_source;

    /**
     * SuperDooperHighAvailableCurrencySource constructor.
     *
     * @param CurrencySource[] $sources
     */
    public function __construct(array $sources)
    {
        \Webmozart\Assert\Assert::allIsInstanceOf($sources, CurrencySource::class);
        $this->sources = $sources;
    }


    public function successfulSource() : CachedCurrencySource
    {

        if ($this->successful_source === null) {

            foreach ($this->sources as $source) {

                $source = new CachedCurrencySource($source);

                try {

                    $source->euro();

                    $this->successful_source = $source;

                } catch (SourceNotAvailableException $e) {

                    //тут можно в лог записать

                }


            }

            if ($this->successful_source === null) {
                throw new SourceNotAvailableException();
            }

        }

        return $this->successful_source;

    }


    public function euro() : Rate
    {
        return $this->successfulSource()->euro();
    }


    public function url() : Url
    {
        return $this->successfulSource()->url();
    }

}


class CurrencyHtmlOutput
{

    /**
     * @var CurrencySource
     */
    private $source;

    /**
     * CurrencyOutput constructor.
     *
     * @param CurrencySource $source
     */
    public function __construct(CurrencySource $source)
    {
        $this->source = $source;
    }

    public function html(): string
    {

        try {
            return 'Курс евро равен <strong>' . $this->source->euro()->toString() . '</strong> по данным <strong>' . $this->source->url()->toString() . '</strong>';
        } catch (SourceNotAvailableException $e) {
            return 'Не удалось получить курс евро. Источник данных недоступен.';
        }

    }


}


echo (new CurrencyHtmlOutput(
    new SuperDooperHighAvailableCurrencySource([
        new CBRFCurrencySource(),
        new ECBCurrencySource()
    ])
))->html();
Таким образом, чтобы добавить новый источник, нужно написать новый класс и добавить его в список источников.

Для тестов можно сделать тестовые сервера на docker или подсовывать вместо url пути
к локальным json/xml файлам с данными о курсах валют.

Для новых валют можно добавлять новые методы, например CurrencySource::dollar()

Ответить