Add basic import of opml, display feed and buckets

This commit is contained in:
Daniel Siepmann 2020-05-26 22:11:02 +02:00
parent ae5c02cfdd
commit 39ebf6bbc1
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
26 changed files with 986 additions and 6 deletions

5
.env.test Normal file
View file

@ -0,0 +1,5 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther

6
.gitignore vendored
View file

@ -9,3 +9,9 @@
/vendor/ /vendor/
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
/node_modules/ /node_modules/
###> symfony/phpunit-bridge ###
.phpunit
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###

View file

@ -1,4 +1,4 @@
body > main { body > main {
display: grid; display: grid;
grid-template-columns: var(--width-sidebar-max) 100%; grid-template-columns: var(--width-sidebar-max) calc(100% - var(--width-sidebar-max));
} }

13
bin/phpunit Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env php
<?php
if (!file_exists(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
if (false === getenv('SYMFONY_PHPUNIT_DIR')) {
putenv('SYMFONY_PHPUNIT_DIR='.__DIR__.'/.phpunit');
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';

View file

@ -12,6 +12,7 @@
"symfony/flex": "^1.3.1", "symfony/flex": "^1.3.1",
"symfony/framework-bundle": "5.0.*", "symfony/framework-bundle": "5.0.*",
"symfony/orm-pack": "^1.0", "symfony/orm-pack": "^1.0",
"symfony/test-pack": "^1.0",
"symfony/twig-pack": "^1.0", "symfony/twig-pack": "^1.0",
"symfony/yaml": "5.0.*" "symfony/yaml": "5.0.*"
}, },

324
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e758c06757d10ff8c2fbac8950b983a3", "content-hash": "137126b022b07b543848195132ab15ea",
"packages": [ "packages": [
{ {
"name": "doctrine/annotations", "name": "doctrine/annotations",
@ -1772,6 +1772,79 @@
], ],
"time": "2020-04-12T14:40:17+00:00" "time": "2020-04-12T14:40:17+00:00"
}, },
{
"name": "symfony/browser-kit",
"version": "v5.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
"reference": "0fa03cfaf1155eedbef871eef1a64c427e624c56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/0fa03cfaf1155eedbef871eef1a64c427e624c56",
"reference": "0fa03cfaf1155eedbef871eef1a64c427e624c56",
"shasum": ""
},
"require": {
"php": "^7.2.5",
"symfony/dom-crawler": "^4.4|^5.0"
},
"require-dev": {
"symfony/css-selector": "^4.4|^5.0",
"symfony/http-client": "^4.4|^5.0",
"symfony/mime": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0"
},
"suggest": {
"symfony/process": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\BrowserKit\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony BrowserKit Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-03-30T11:42:42+00:00"
},
{ {
"name": "symfony/cache", "name": "symfony/cache",
"version": "v5.0.8", "version": "v5.0.8",
@ -2091,6 +2164,73 @@
], ],
"time": "2020-03-30T11:42:42+00:00" "time": "2020-03-30T11:42:42+00:00"
}, },
{
"name": "symfony/css-selector",
"version": "v5.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "5f8d5271303dad260692ba73dfa21777d38e124e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/5f8d5271303dad260692ba73dfa21777d38e124e",
"reference": "5f8d5271303dad260692ba73dfa21777d38e124e",
"shasum": ""
},
"require": {
"php": "^7.2.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-03-27T16:56:45+00:00"
},
{ {
"name": "symfony/dependency-injection", "name": "symfony/dependency-injection",
"version": "v5.0.8", "version": "v5.0.8",
@ -2288,6 +2428,81 @@
], ],
"time": "2020-04-12T16:45:47+00:00" "time": "2020-04-12T16:45:47+00:00"
}, },
{
"name": "symfony/dom-crawler",
"version": "v5.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "892311d23066844a267ac1a903d8a9d79968a1a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/892311d23066844a267ac1a903d8a9d79968a1a7",
"reference": "892311d23066844a267ac1a903d8a9d79968a1a7",
"shasum": ""
},
"require": {
"php": "^7.2.5",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"masterminds/html5": "<2.6"
},
"require-dev": {
"masterminds/html5": "^2.6",
"symfony/css-selector": "^4.4|^5.0"
},
"suggest": {
"symfony/css-selector": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\DomCrawler\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-03-30T11:42:42+00:00"
},
{ {
"name": "symfony/dotenv", "name": "symfony/dotenv",
"version": "v5.0.8", "version": "v5.0.8",
@ -3187,6 +3402,85 @@
"description": "A pack for the Doctrine ORM", "description": "A pack for the Doctrine ORM",
"time": "2020-02-10T18:03:48+00:00" "time": "2020-02-10T18:03:48+00:00"
}, },
{
"name": "symfony/phpunit-bridge",
"version": "v5.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/phpunit-bridge.git",
"reference": "00b8da18a52fa842b7a39613fb0a63720a354e74"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/00b8da18a52fa842b7a39613fb0a63720a354e74",
"reference": "00b8da18a52fa842b7a39613fb0a63720a354e74",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
},
"conflict": {
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0|<6.4,>=6.0|9.1.2"
},
"suggest": {
"symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader"
},
"bin": [
"bin/simple-phpunit"
],
"type": "symfony-bridge",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
},
"thanks": {
"name": "phpunit/phpunit",
"url": "https://github.com/sebastianbergmann/phpunit"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Bridge\\PhpUnit\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony PHPUnit Bridge",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-04-28T17:58:55+00:00"
},
{ {
"name": "symfony/polyfill-intl-idn", "name": "symfony/polyfill-intl-idn",
"version": "v1.17.0", "version": "v1.17.0",
@ -3620,6 +3914,34 @@
], ],
"time": "2020-03-27T16:56:45+00:00" "time": "2020-03-27T16:56:45+00:00"
}, },
{
"name": "symfony/test-pack",
"version": "v1.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/test-pack.git",
"reference": "ff87e800a67d06c423389f77b8209bc9dc469def"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/test-pack/zipball/ff87e800a67d06c423389f77b8209bc9dc469def",
"reference": "ff87e800a67d06c423389f77b8209bc9dc469def",
"shasum": ""
},
"require": {
"php": "^7.0",
"symfony/browser-kit": "*",
"symfony/css-selector": "*",
"symfony/phpunit-bridge": "*"
},
"type": "symfony-pack",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A pack for functional and end-to-end testing within a Symfony app",
"time": "2019-06-21T06:27:32+00:00"
},
{ {
"name": "symfony/translation-contracts", "name": "symfony/translation-contracts",
"version": "v2.0.1", "version": "v2.0.1",

View file

@ -8,3 +8,7 @@ start:
bucket: bucket:
path: /bucket/{slug} path: /bucket/{slug}
controller: App\Controller\BucketController::show controller: App\Controller\BucketController::show
feed:
path: /feed/{slug}
controller: App\Controller\FeedController::show

View file

@ -22,3 +22,12 @@ services:
App\Controller\: App\Controller\:
resource: '../src/Controller' resource: '../src/Controller'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']
App\Command\ImportOpmlfileCommand:
tags:
- name: 'console.command'
command: 'app:import:opmlfile'
slugger:
class: App\Service\Slugger

33
phpunit.xml.dist Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="bin/.phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="7.5" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
</phpunit>

View file

@ -59,4 +59,4 @@ body > footer {
body > main { body > main {
display: grid; display: grid;
grid-template-columns: var(--width-sidebar-max) 100%; } grid-template-columns: var(--width-sidebar-max) calc(100% - var(--width-sidebar-max)); }

View file

@ -0,0 +1,39 @@
<?php
namespace App\Command;
use App\Service\OpmlImporter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ImportOpmlfileCommand extends Command
{
/**
* @var OpmlImporter
*/
private $importer;
public function __construct(
OpmlImporter $importer
) {
parent::__construct();
$this->importer = $importer;
}
protected function configure()
{
$this->setDescription('Imports an OPML file.');
$this->addArgument('file', InputArgument::REQUIRED, 'The OPML file to import.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->importer->importFromFile(
$input->getArgument('file')
);
return 0;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Controller;
use App\Entity\Feed;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class FeedController extends AbstractController
{
public function show(Feed $feed)
{
return $this->render('feed/show.html.twig', [
'feed' => $feed,
]);
}
}

View file

@ -3,6 +3,8 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\BucketRepository; use App\Repository\BucketRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** /**
@ -28,12 +30,18 @@ class Bucket
*/ */
private $slug; private $slug;
/**
* @ORM\OneToMany(targetEntity=Feed::class, mappedBy="bucket")
*/
private $feeds;
public function __construct( public function __construct(
string $name, string $name,
string $slug string $slug
) { ) {
$this->name = $name; $this->name = $name;
$this->slug = $slug; $this->slug = $slug;
$this->feeds = new ArrayCollection();
} }
public function getId(): int public function getId(): int
@ -50,4 +58,12 @@ class Bucket
{ {
return $this->slug; return $this->slug;
} }
/**
* @return Collection|Feed[]
*/
public function getFeeds(): Collection
{
return $this->feeds;
}
} }

89
src/Entity/Feed.php Normal file
View file

@ -0,0 +1,89 @@
<?php
namespace App\Entity;
use App\Repository\FeedRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=FeedRepository::class)
* @ORM\Table(uniqueConstraints={@ORM\UniqueConstraint(name="feed_routing", columns={"slug"})})
*/
class Feed
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="string", length=255)
*/
private $slug;
/**
* @ORM\Column(type="string", length=255)
*/
private $url;
/**
* @ORM\Column(type="string", length=255)
*/
private $htmlUrl;
/**
* @ORM\ManyToOne(targetEntity=Bucket::class, inversedBy="feeds")
*/
private $bucket;
public function __construct(
string $name,
string $slug,
string $url,
Bucket $bucket = null,
string $htmlUrl = ''
) {
$this->name = $name;
$this->slug = $slug;
$this->url = $url;
$this->bucket = $bucket;
$this->htmlUrl = $htmlUrl;
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getUrl(): string
{
return $this->url;
}
public function getSlug(): string
{
return $this->slug;
}
public function getBucket(): Bucket
{
return $this->bucket;
}
public function getHtmlUrl(): string
{
return $this->htmlUrl;
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200526192352 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('CREATE TABLE feed (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, bucket_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, url VARCHAR(255) NOT NULL, html_url VARCHAR(255) NOT NULL)');
$this->addSql('CREATE INDEX IDX_234044AB84CE584D ON feed (bucket_id)');
$this->addSql('CREATE UNIQUE INDEX feed_routing ON feed (slug)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('DROP TABLE feed');
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Repository;
use App\Entity\Feed;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Feed|null find($id, $lockMode = null, $lockVersion = null)
* @method Feed|null findOneBy(array $criteria, array $orderBy = null)
* @method Feed[] findAll()
* @method Feed[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class FeedRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Feed::class);
}
// /**
// * @return Feed[] Returns an array of Feed objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('f')
->andWhere('f.exampleField = :val')
->setParameter('val', $value)
->orderBy('f.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Feed
{
return $this->createQueryBuilder('f')
->andWhere('f.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

View file

@ -0,0 +1,130 @@
<?php
namespace App\Service;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use App\Entity\Bucket;
use App\Entity\Feed;
use Doctrine\ORM\EntityManagerInterface;
use SimpleXMLElement;
use Symfony\Component\String\Slugger\SluggerInterface;
class OpmlImporter
{
/**
* @var SluggerInterface
*/
private $slugger;
/**
* @var EntityManagerInterface
*/
private $entityManager;
public function __construct(
SluggerInterface $slugger,
EntityManagerInterface $entityManager
) {
$this->slugger = $slugger;
$this->entityManager = $entityManager;
}
public function importFromFile(string $filePath): void
{
$opml = new SimpleXMLElement(file_get_contents($filePath));
// TODO: Trigger event
foreach ($opml->body->outline as $outline) {
$this->importOutline($outline);
}
$this->entityManager->flush();
}
public function isBucket(SimpleXMLElement $outline): bool
{
return is_null($outline['type'])
&& (
isset($outline['text'])
|| isset($outline['title'])
)
;
}
public function isFeed(SimpleXMLElement $outline): bool
{
return isset($outline['type'])
&& $outline['type']->__toString() === 'rss'
&& isset($outline['xmlUrl'])
;
}
private function importOutline(SimpleXMLElement $outline, Bucket $bucket = null): void
{
if ($this->isBucket($outline)) {
$this->importBucket($outline);
return;
}
if ($this->isFeed($outline)) {
$this->importFeed($outline, $bucket);
return;
}
}
private function importBucket(SimpleXMLElement $outline): void
{
$title = $outline['title'] ?? $outline['text'];
// Check whether bucket already exists.
$bucket = new Bucket(
$title,
strtolower($this->slugger->slug($title))
);
// TODO: Trigger event
$this->entityManager->persist($bucket);
foreach ($outline->outline as $subOutline) {
$this->importOutline($subOutline, $bucket);
}
}
private function importFeed(SimpleXMLElement $outline, Bucket $bucket = null): void
{
$title = $outline['title'] ?? $outline['text'];
$feed = new Feed(
$title,
strtolower($this->slugger->slug($title)),
$outline['xmlUrl'],
$bucket,
$outline['htmlUrl'] ?? ''
);
// TODO: Trigger event
$this->entityManager->persist($feed);
}
}

38
src/Service/Slugger.php Normal file
View file

@ -0,0 +1,38 @@
<?php
namespace App\Service;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use Symfony\Component\String\AbstractUnicodeString;
use Symfony\Component\String\Slugger\AsciiSlugger;
class Slugger extends AsciiSlugger
{
public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString
{
$string .= '-' . uniqid();
$slug = parent::slug($string, $separator, $locale);
$slug = $slug->lower();
return $slug;
}
}

View file

@ -132,6 +132,9 @@
"symfony/asset": { "symfony/asset": {
"version": "v5.0.8" "version": "v5.0.8"
}, },
"symfony/browser-kit": {
"version": "v5.0.8"
},
"symfony/cache": { "symfony/cache": {
"version": "v5.0.8" "version": "v5.0.8"
}, },
@ -154,12 +157,18 @@
"config/bootstrap.php" "config/bootstrap.php"
] ]
}, },
"symfony/css-selector": {
"version": "v5.0.8"
},
"symfony/dependency-injection": { "symfony/dependency-injection": {
"version": "v5.0.8" "version": "v5.0.8"
}, },
"symfony/doctrine-bridge": { "symfony/doctrine-bridge": {
"version": "v5.0.8" "version": "v5.0.8"
}, },
"symfony/dom-crawler": {
"version": "v5.0.8"
},
"symfony/dotenv": { "symfony/dotenv": {
"version": "v5.0.8" "version": "v5.0.8"
}, },
@ -231,6 +240,21 @@
"symfony/orm-pack": { "symfony/orm-pack": {
"version": "v1.0.8" "version": "v1.0.8"
}, },
"symfony/phpunit-bridge": {
"version": "4.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "4.3",
"ref": "6d0e35f749d5f4bfe1f011762875275cd3f9874f"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/polyfill-intl-grapheme": { "symfony/polyfill-intl-grapheme": {
"version": "v1.17.0" "version": "v1.17.0"
}, },
@ -272,6 +296,9 @@
"symfony/string": { "symfony/string": {
"version": "v5.0.8" "version": "v5.0.8"
}, },
"symfony/test-pack": {
"version": "v1.0.6"
},
"symfony/translation-contracts": { "symfony/translation-contracts": {
"version": "v2.0.1" "version": "v2.0.1"
}, },

View file

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title> <title>{% block title %}{{ app_name }}{% endblock %}</title>
{% block stylesheets %} {% block stylesheets %}
<link href="{{ asset('css/style.css') }}" rel="stylesheet"> <link href="{{ asset('css/style.css') }}" rel="stylesheet">
{% endblock %} {% endblock %}

View file

@ -1,9 +1,22 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Hello BucketController!{% endblock %} {% block title %}{{ bucket.name }} | {{ parent() }}{% endblock %}
{% block body %} {% block body %}
<header> <header>
<h1>Bucket: {{ bucket.name }}</h1> <h1>Bucket: {{ bucket.name }} <small>with {{ bucket.feeds | length }} feeds</small></h1>
</header> </header>
{{ include('feed/entries.html.twig') }}
<aside>
<h1>Feeds in this bucket</h1>
<ul>
{% for feed in bucket.feeds %}
<li>
<a href="{{ path('feed', {slug: feed.slug}) }}">{{ feed.name }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,2 @@
<p>TODO: Show latest entries, need to be provided via include twig call</p>
<p>TODO: Create command to fetch entries</p>

View file

@ -0,0 +1,20 @@
{% extends 'base.html.twig' %}
{% block title %}Hello FeedController!{% endblock %}
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
This friendly message is coming from:
<ul>
<li>Your controller at <code><a href="{{ '/home/daniels/Projects/own/rss/project/src/Controller/FeedController.php'|file_link(0) }}">src/Controller/FeedController.php</a></code></li>
<li>Your template at <code><a href="{{ '/home/daniels/Projects/own/rss/project/templates/feed/index.html.twig'|file_link(0) }}">templates/feed/index.html.twig</a></code></li>
</ul>
</div>
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends 'base.html.twig' %}
{% block title %}{{ feed.name }} {{ feed.bucket.name }}| {{ parent() }}{% endblock %}
{% block body %}
<header>
<h1>Feed: {{ feed.name }}</h1>
<nav>
<a href="{{ feed.url }}" target="_blank"
rel="external nofollow noopener noreferrer"
referrerpolicy="no-referrer">Feed URL</a>
{% if feed.htmlUrl %}
<a href="{{ feed.htmlUrl }}" target="_blank"
rel="external nofollow noopener noreferrer"
referrerpolicy="no-referrer">HTML variant</a>
{% endif %}
<a href="{{ path('bucket', {slug: feed.bucket.slug}) }}">{{ feed.bucket.name }} bucket</a>
</nav>
</header>
{{ include('feed/entries.html.twig') }}
{% endblock %}

View file

@ -0,0 +1,73 @@
<?php
namespace App\Tests\Service;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use App\Service\OpmlImporter;
use PHPUnit\Framework\TestCase;
use SimpleXMLElement;
class OpmlImporterTest extends TestCase
{
/**
* @test
*/
public function detectsBucket()
{
$subject = new OpmlImporter();
$simpleXmlElement = new SimpleXmlElement('<outline title="Some title"></outline>');
static::assertTrue($subject->isBucket($simpleXmlElement));
}
/**
* @test
*/
public function detectsFeedIsNoBucket()
{
$subject = new OpmlImporter();
$simpleXmlElement = new SimpleXmlElement('<outline type="rss" xmlUrl="http://typo3.org/xml-feeds/rss.xml"/>');
static::assertFalse($subject->isBucket($simpleXmlElement));
}
/**
* @test
*/
public function detectsFeed()
{
$subject = new OpmlImporter();
$simpleXmlElement = new SimpleXmlElement('<outline type="rss" xmlUrl="http://typo3.org/xml-feeds/rss.xml"/>');
static::assertTrue($subject->isFeed($simpleXmlElement));
}
/**
* @test
*/
public function detectsBucketIsNoFeed()
{
$subject = new OpmlImporter();
$simpleXmlElement = new SimpleXmlElement('<outline title="Some title"></outline>');
static::assertFalse($subject->isFeed($simpleXmlElement));
}
}

11
tests/bootstrap.php Normal file
View file

@ -0,0 +1,11 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (file_exists(dirname(__DIR__).'/config/bootstrap.php')) {
require dirname(__DIR__).'/config/bootstrap.php';
} elseif (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}