mirror of
https://github.com/Chouchen/ShikiryuRSS.git
synced 2024-11-24 20:28:51 +01:00
🦺 Add validator
This commit is contained in:
parent
bc0e818bbc
commit
fec8c122e3
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
/vendor/
|
/vendor/
|
||||||
/.idea
|
/.idea
|
||||||
|
/tests/error.log
|
||||||
|
@ -2,39 +2,103 @@
|
|||||||
|
|
||||||
namespace Shikiryu\SRSS\Entity;
|
namespace Shikiryu\SRSS\Entity;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
use Shikiryu\SRSS\Entity\Channel\Image;
|
use Shikiryu\SRSS\Entity\Channel\Image;
|
||||||
|
use Shikiryu\SRSS\Validator\HasValidator;
|
||||||
|
use Shikiryu\SRSS\Validator\Validator;
|
||||||
|
|
||||||
class Channel implements SRSSElement
|
/**
|
||||||
|
* https://cyber.harvard.edu/rss/rss.html#requiredChannelElements
|
||||||
|
*/
|
||||||
|
class Channel extends HasValidator implements SRSSElement
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
public string $title;
|
public string $title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
public string $link;
|
public string $link;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @required
|
||||||
|
*/
|
||||||
public string $description;
|
public string $description;
|
||||||
|
|
||||||
public ?string $language;
|
/**
|
||||||
public ?string $copyright;
|
* @lang
|
||||||
public ?string $managingEditor;
|
*/
|
||||||
public ?string $webMaster;
|
public ?string $language = null;
|
||||||
public ?string $pubDate;
|
/**
|
||||||
public ?string $lastBuildDate;
|
* @nohtml
|
||||||
public ?string $category;
|
*/
|
||||||
public ?string $generator;
|
public ?string $copyright = null;
|
||||||
public ?string $docs;
|
/**
|
||||||
public ?string $cloud;
|
* @nohtml
|
||||||
public ?string $ttl;
|
*/
|
||||||
public ?Image $image;
|
public ?string $managingEditor = null;
|
||||||
public ?string $rating;
|
/**
|
||||||
public ?string $textInput;
|
* @nohtml
|
||||||
public ?string $skipHours;
|
*/
|
||||||
public ?string $skipDays;
|
public ?string $webMaster = null;
|
||||||
|
/**
|
||||||
public array $required = ['title', 'link', 'description'];
|
* @date
|
||||||
|
*/
|
||||||
|
public ?string $pubDate = null;
|
||||||
|
/**
|
||||||
|
* @date
|
||||||
|
*/
|
||||||
|
public ?string $lastBuildDate = null;
|
||||||
|
/**
|
||||||
|
* TODO should be an array
|
||||||
|
*/
|
||||||
|
public ?string $category = null;
|
||||||
|
/**
|
||||||
|
* @nohtml
|
||||||
|
*/
|
||||||
|
public ?string $generator = null;
|
||||||
|
/**
|
||||||
|
* @url
|
||||||
|
*/
|
||||||
|
public ?string $docs = null;
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
* TODO validator
|
||||||
|
*/
|
||||||
|
public ?string $cloud = null;
|
||||||
|
/**
|
||||||
|
* @int
|
||||||
|
*/
|
||||||
|
public ?string $ttl = null;
|
||||||
|
public ?Image $image = null;
|
||||||
|
public ?string $rating = null;
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
* The purpose of the <textInput> element is something of a mystery. You can use it to specify a search engine box. Or to allow a reader to provide feedback. Most aggregators ignore it.
|
||||||
|
*/
|
||||||
|
public ?string $textInput = null;
|
||||||
|
/**
|
||||||
|
* @hour
|
||||||
|
*/
|
||||||
|
public ?string $skipHours = null;
|
||||||
|
/**
|
||||||
|
* @day
|
||||||
|
*/
|
||||||
|
public ?string $skipDays = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
|
* @throws ReflectionException
|
||||||
*/
|
*/
|
||||||
public function isValid(): bool
|
public function isValid(): bool
|
||||||
{
|
{
|
||||||
return count(array_filter($this->required, fn($field) => !empty($this->{$field}))) === 0;
|
$annotation_validation = new Validator();
|
||||||
|
|
||||||
|
return $annotation_validation->isObjectValid($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
19
src/SRSS.php
19
src/SRSS.php
@ -8,11 +8,10 @@ use Shikiryu\SRSS\Entity\Item;
|
|||||||
|
|
||||||
class SRSS implements Iterator
|
class SRSS implements Iterator
|
||||||
{
|
{
|
||||||
public array $items; // array of SRSSItems
|
|
||||||
protected $attr; // array of RSS attributes
|
|
||||||
private $position; // Iterator position
|
|
||||||
|
|
||||||
public Channel $channel;
|
public Channel $channel;
|
||||||
|
public array $items; // array of SRSSItems
|
||||||
|
|
||||||
|
private int $position; // Iterator position
|
||||||
|
|
||||||
// lists of possible attributes for RSS
|
// lists of possible attributes for RSS
|
||||||
protected $possibleAttr = [
|
protected $possibleAttr = [
|
||||||
@ -43,7 +42,6 @@ class SRSS implements Iterator
|
|||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->attr = [];
|
|
||||||
$this->items = [];
|
$this->items = [];
|
||||||
$this->position = 0;
|
$this->position = 0;
|
||||||
}
|
}
|
||||||
@ -76,9 +74,8 @@ class SRSS implements Iterator
|
|||||||
/**
|
/**
|
||||||
* check if current RSS is a valid one (based on specifications)
|
* check if current RSS is a valid one (based on specifications)
|
||||||
* @return bool
|
* @return bool
|
||||||
* TODO use required
|
|
||||||
*/
|
*/
|
||||||
public function isValid()
|
public function isValid(): bool
|
||||||
{
|
{
|
||||||
$valid = true;
|
$valid = true;
|
||||||
$items = $this->getItems();
|
$items = $this->getItems();
|
||||||
@ -115,7 +112,7 @@ class SRSS implements Iterator
|
|||||||
*/
|
*/
|
||||||
public function __set($name, $val)
|
public function __set($name, $val)
|
||||||
{
|
{
|
||||||
if (!array_key_exists($name, $this->possibleAttr)) {
|
if (!property_exists(Channel::class, $name)) {
|
||||||
throw new SRSSException($name . ' is not a possible item');
|
throw new SRSSException($name . ' is not a possible item');
|
||||||
}
|
}
|
||||||
$flag = $this->possibleAttr[$name];
|
$flag = $this->possibleAttr[$name];
|
||||||
@ -148,7 +145,7 @@ class SRSS implements Iterator
|
|||||||
/**
|
/**
|
||||||
* current from Iterator
|
* current from Iterator
|
||||||
*/
|
*/
|
||||||
public function current()
|
public function current(): mixed
|
||||||
{
|
{
|
||||||
return $this->items[$this->position];
|
return $this->items[$this->position];
|
||||||
}
|
}
|
||||||
@ -179,7 +176,7 @@ class SRSS implements Iterator
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* getter of 1st item
|
* getter of 1st item
|
||||||
* @return Item
|
* @return Item|null
|
||||||
*/
|
*/
|
||||||
public function getFirst(): ?Item
|
public function getFirst(): ?Item
|
||||||
{
|
{
|
||||||
@ -214,7 +211,6 @@ class SRSS implements Iterator
|
|||||||
/**
|
/**
|
||||||
* getter of all items
|
* getter of all items
|
||||||
* @return Item[]
|
* @return Item[]
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
*/
|
||||||
public function getItems(): array
|
public function getItems(): array
|
||||||
{
|
{
|
||||||
@ -224,7 +220,6 @@ class SRSS implements Iterator
|
|||||||
/**
|
/**
|
||||||
* transform current object into an array
|
* transform current object into an array
|
||||||
* @return array
|
* @return array
|
||||||
* @throws SRSSException
|
|
||||||
*/
|
*/
|
||||||
public function toArray(): array
|
public function toArray(): array
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@ use DOMNodeList;
|
|||||||
use DOMXPath;
|
use DOMXPath;
|
||||||
use Shikiryu\SRSS\Entity\Channel;
|
use Shikiryu\SRSS\Entity\Channel;
|
||||||
use Shikiryu\SRSS\Entity\Channel\Image;
|
use Shikiryu\SRSS\Entity\Channel\Image;
|
||||||
|
use Shikiryu\SRSS\Entity\Item;
|
||||||
|
|
||||||
class SRSSParser extends DomDocument
|
class SRSSParser extends DomDocument
|
||||||
{
|
{
|
||||||
@ -57,10 +58,10 @@ class SRSSParser extends DomDocument
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array|mixed
|
* @return Item[]
|
||||||
* @throws \Shikiryu\SRSS\SRSSException
|
* @throws \Shikiryu\SRSS\SRSSException
|
||||||
*/
|
*/
|
||||||
private function getItems()
|
private function getItems(): mixed
|
||||||
{
|
{
|
||||||
$channel = $this->_getChannel();
|
$channel = $this->_getChannel();
|
||||||
/** @var DOMNodeList $items */
|
/** @var DOMNodeList $items */
|
||||||
|
@ -6,26 +6,23 @@ class SRSSTools
|
|||||||
{
|
{
|
||||||
public static function check($check, $flag)
|
public static function check($check, $flag)
|
||||||
{
|
{
|
||||||
switch($flag){
|
return match ($flag) {
|
||||||
case 'nohtml': return self::noHTML($check);
|
'nohtml' => self::noHTML($check),
|
||||||
case 'link': return self::checkLink($check);
|
'link' => self::checkLink($check),
|
||||||
case 'html': return self::HTML4XML($check);
|
'html' => self::HTML4XML($check),
|
||||||
/*case 'lang':
|
'date' => self::getRSSDate($check),
|
||||||
return self::noHTML($check);
|
'email' => self::checkEmail($check),
|
||||||
*/
|
'int' => self::checkInt($check),
|
||||||
case 'date': return self::getRSSDate($check);
|
'hour' => self::checkHour($check),
|
||||||
case 'email': return self::checkEmail($check);
|
'day' => self::checkDay($check),
|
||||||
case 'int': return self::checkInt($check);
|
'folder' => [],
|
||||||
case 'hour': return self::checkHour($check);
|
'media_type' => self::checkMediaType($check),
|
||||||
case 'day': return self::checkDay($check);
|
'media_medium' => self::checkMediaMedium($check),
|
||||||
case 'folder': return [];
|
'bool' => self::checkBool($check),
|
||||||
case 'media_type': return self::checkMediaType($check);
|
'medium_expression' => self::checkMediumExpression($check),
|
||||||
case 'media_medium': return self::checkMediaMedium($check);
|
'' => $check,
|
||||||
case 'bool': return self::checkBool($check);
|
default => throw new SRSSException('flag ' . $flag . ' does not exist.'),
|
||||||
case 'medium_expression': return self::checkMediumExpression($check);
|
};
|
||||||
case '': return $check;
|
|
||||||
default: throw new SRSSException('flag '.$flag.' does not exist.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
11
src/Validator/HasValidator.php
Normal file
11
src/Validator/HasValidator.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Validator;
|
||||||
|
|
||||||
|
abstract class HasValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var bool[]
|
||||||
|
*/
|
||||||
|
public array $validated = [];
|
||||||
|
}
|
146
src/Validator/Validator.php
Normal file
146
src/Validator/Validator.php
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Shikiryu\SRSS\Validator;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
|
||||||
|
class Validator
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function isObjectValid($object): bool
|
||||||
|
{
|
||||||
|
if (!$object->validated) {
|
||||||
|
$object = $this->validateObject($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !in_array(false, $object->validated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function validateObject($object)
|
||||||
|
{
|
||||||
|
$properties = $this->_getClassProperties(get_class($object));
|
||||||
|
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
$propertyValue = $object->{$property->name};
|
||||||
|
$propertyAnnotations = $this->_getPropertyAnnotations($property, get_class($object));
|
||||||
|
|
||||||
|
if (!in_array('required', $propertyAnnotations) && empty($propertyValue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($propertyAnnotations as $propertyAnnotation) {
|
||||||
|
$annotation = explode(' ', $propertyAnnotation);
|
||||||
|
|
||||||
|
$object->validated[$property->name] = $this->_validateProperty($annotation, $propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateProperty(array $annotation, $property): bool
|
||||||
|
{
|
||||||
|
if (count($annotation) === 1) {
|
||||||
|
return call_user_func([$this, sprintf('_validate%s', ucfirst($annotation[0]))], $property);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // TODO check
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
private function _getClassProperties($class): array
|
||||||
|
{
|
||||||
|
$ReflectionClass = new ReflectionClass($class);
|
||||||
|
|
||||||
|
return $ReflectionClass->getProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _getPropertyAnnotations($property, $className): array
|
||||||
|
{
|
||||||
|
preg_match_all('#@(.*?)\n#s', $property->getDocComment(), $annotations);
|
||||||
|
|
||||||
|
return array_map(fn($annotation) => trim($annotation), $annotations[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateString($value): bool
|
||||||
|
{
|
||||||
|
return is_string($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateInt($value): bool
|
||||||
|
{
|
||||||
|
return is_numeric($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateRequired($value): bool
|
||||||
|
{
|
||||||
|
return !empty(trim($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
* @return bool
|
||||||
|
* https://cyber.harvard.edu/rss/languages.html
|
||||||
|
*/
|
||||||
|
private function _validateLang($value): bool
|
||||||
|
{
|
||||||
|
return in_array(strtolower($value), [
|
||||||
|
'af','sq','eu','be','bg','ca','zh-cn','zh-tw','hr','cs','da','nl','nl-be','nl-nl','en','en-au','en-bz',
|
||||||
|
'en-ca','en-ie','en-jm','en-nz','en-ph','en-za','en-tt','en-gb','en-us','en-zw','et','fo','fi','fr','fr-be',
|
||||||
|
'fr-ca','fr-fr','fr-lu','fr-mc','fr-ch','gl','gd','de','de-at','de-de','de-li','de-lu','de-ch','el','haw',
|
||||||
|
'hu','is','in','ga','it','it-it','it-ch','ja','ko','mk','no','pl','pt','pt-br','pt-pt','ro','ro-mo','ro-ro',
|
||||||
|
'ru','ru-mo','ru-ru','sr','sk','sl','es','es-ar','es-bo','es-cl','es-co','es-cr','es-do','es-ec','es-sv',
|
||||||
|
'es-gt','es-hn','es-mx','es-ni','es-pa','es-py','es-pe','es-pr','es-es','es-uy','es-ve','sv','sv-fi','sv-se',
|
||||||
|
'tr','uk',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function _validateNoHtml($value): bool
|
||||||
|
{
|
||||||
|
return strip_tags($value) === $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateUrl($value): bool
|
||||||
|
{
|
||||||
|
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateDate($value): bool
|
||||||
|
{
|
||||||
|
return \DateTime::createFromFormat(DateTimeInterface::RSS, $value) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateHour($value): bool
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'options' => [
|
||||||
|
'default' => 0,
|
||||||
|
'min_range' => 0,
|
||||||
|
'max_range' => 23
|
||||||
|
]
|
||||||
|
];
|
||||||
|
return filter_var($value, FILTER_VALIDATE_INT, $options) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function _validateDay($value): bool
|
||||||
|
{
|
||||||
|
return in_array(
|
||||||
|
strtolower($value),
|
||||||
|
['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,8 @@ class BasicReader extends TestCase
|
|||||||
$first_item = $rss->getFirst();
|
$first_item = $rss->getFirst();
|
||||||
self::assertNotNull($first_item);
|
self::assertNotNull($first_item);
|
||||||
self::assertEquals('RSS Tutorial', $first_item->title);
|
self::assertEquals('RSS Tutorial', $first_item->title);
|
||||||
|
|
||||||
|
self::assertTrue($rss->channel->isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testRssNotFound()
|
public function testRssNotFound()
|
||||||
@ -38,5 +40,7 @@ class BasicReader extends TestCase
|
|||||||
self::assertEquals('Star City', $rss->getFirst()->title);
|
self::assertEquals('Star City', $rss->getFirst()->title);
|
||||||
self::assertEquals('http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp', $rss->getLast()->link);
|
self::assertEquals('http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp', $rss->getLast()->link);
|
||||||
self::assertEquals('Fri, 30 May 2003 11:06:42 GMT', $rss->getItem(2)->pubDate);
|
self::assertEquals('Fri, 30 May 2003 11:06:42 GMT', $rss->getItem(2)->pubDate);
|
||||||
|
|
||||||
|
self::assertTrue($rss->channel->isValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ class MediaTest extends TestCase
|
|||||||
self::assertEquals('Kirstie Alley, \'Cheers\' and \'Veronica\'s Closet\' star, dead at 71', $first_item->title);
|
self::assertEquals('Kirstie Alley, \'Cheers\' and \'Veronica\'s Closet\' star, dead at 71', $first_item->title);
|
||||||
|
|
||||||
self::assertEquals('https://cdn.cnn.com/cnnnext/dam/assets/221205172141-kirstie-alley-2005-super-169.jpg', $first_item->medias[0]->url);
|
self::assertEquals('https://cdn.cnn.com/cnnnext/dam/assets/221205172141-kirstie-alley-2005-super-169.jpg', $first_item->medias[0]->url);
|
||||||
|
self::assertTrue($rss->channel->isValid(), var_export($rss->channel->validated, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMusicVideo()
|
public function testMusicVideo()
|
||||||
@ -25,5 +26,6 @@ class MediaTest extends TestCase
|
|||||||
|
|
||||||
$first_item = $rss->getFirst();
|
$first_item = $rss->getFirst();
|
||||||
self::assertEquals('http://www.foo.com/movie.mov', $first_item->medias[0]->url);
|
self::assertEquals('http://www.foo.com/movie.mov', $first_item->medias[0]->url);
|
||||||
|
self::assertTrue($rss->channel->isValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user