Имеется задача, нужно посетить сайт и спарсить информацию. Если сайт недоступен, то переключиться на другой и спарсить с него. Как реализовать это на ООП с соблюдением solid?
Предполагается, что список сайтов можно расширить.
Обязательно ли при объявлении списка сайтов руководствоваться принципом закрытости,открытости?
Если можно им руководствоваться, то как получить массив с урл этих сайтов?
Прошу нацелить на правильный путь.
Php ООП. Переключение на другой сайт при недоступности первого
-
- Сообщения: 10
- Зарегистрирован: 2019.02.23, 22:55
- samdark
- Администратор
- Сообщения: 9489
- Зарегистрирован: 2009.04.02, 13:46
- Откуда: Воронеж
- Контактная информация:
Re: Php ООП. Переключение на другой сайт при недоступности первого
Давайте с чего-то начнём. Какие классы вы планируете сделать с какими интерфейсами?
Нравится Yii? Давайте сделаем его лучше!.
-
- Сообщения: 10
- Зарегистрирован: 2019.02.23, 22:55
Re: Php ООП. Переключение на другой сайт при недоступности первого
Сделал так, прошу оценить по всем критериям. Тестируемость, 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>';
}
}
Re: Php ООП. Переключение на другой сайт при недоступности первого
1. По мне ParseInterface и его наследники абсолютно лишние, форматы для сайтов строго заданы, какая тут может быть замена со временем?
2. Класс Body тоже по-моему лишний. Для него то отдельная абстракция вообще зачем?
3. По PSR2 код бы форматировать.
Короче, я бы сделал так:
Таким образом, чтобы добавить новый источник, нужно написать новый класс и добавить его в список источников.
Для тестов можно сделать тестовые сервера на docker или подсовывать вместо url пути
к локальным json/xml файлам с данными о курсах валют.
Для новых валют можно добавлять новые методы, например CurrencySource::dollar()
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()