mirror of
https://codeberg.org/danielsiepmann/news-reader-php.git
synced 2024-11-21 11:56:08 +01:00
Add basic import of opml, display feed and buckets
This commit is contained in:
parent
ae5c02cfdd
commit
39ebf6bbc1
26 changed files with 986 additions and 6 deletions
5
.env.test
Normal file
5
.env.test
Normal 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
6
.gitignore
vendored
|
@ -9,3 +9,9 @@
|
|||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
/node_modules/
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit
|
||||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
body > main {
|
||||
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
13
bin/phpunit
Executable 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';
|
|
@ -12,6 +12,7 @@
|
|||
"symfony/flex": "^1.3.1",
|
||||
"symfony/framework-bundle": "5.0.*",
|
||||
"symfony/orm-pack": "^1.0",
|
||||
"symfony/test-pack": "^1.0",
|
||||
"symfony/twig-pack": "^1.0",
|
||||
"symfony/yaml": "5.0.*"
|
||||
},
|
||||
|
|
324
composer.lock
generated
324
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e758c06757d10ff8c2fbac8950b983a3",
|
||||
"content-hash": "137126b022b07b543848195132ab15ea",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/annotations",
|
||||
|
@ -1772,6 +1772,79 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v5.0.8",
|
||||
|
@ -2091,6 +2164,73 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v5.0.8",
|
||||
|
@ -2288,6 +2428,81 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v5.0.8",
|
||||
|
@ -3187,6 +3402,85 @@
|
|||
"description": "A pack for the Doctrine ORM",
|
||||
"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",
|
||||
"version": "v1.17.0",
|
||||
|
@ -3620,6 +3914,34 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v2.0.1",
|
||||
|
|
|
@ -8,3 +8,7 @@ start:
|
|||
bucket:
|
||||
path: /bucket/{slug}
|
||||
controller: App\Controller\BucketController::show
|
||||
|
||||
feed:
|
||||
path: /feed/{slug}
|
||||
controller: App\Controller\FeedController::show
|
||||
|
|
|
@ -22,3 +22,12 @@ services:
|
|||
App\Controller\:
|
||||
resource: '../src/Controller'
|
||||
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
33
phpunit.xml.dist
Normal 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>
|
|
@ -59,4 +59,4 @@ body > footer {
|
|||
|
||||
body > main {
|
||||
display: grid;
|
||||
grid-template-columns: var(--width-sidebar-max) 100%; }
|
||||
grid-template-columns: var(--width-sidebar-max) calc(100% - var(--width-sidebar-max)); }
|
||||
|
|
39
src/Command/ImportOpmlfileCommand.php
Normal file
39
src/Command/ImportOpmlfileCommand.php
Normal 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;
|
||||
}
|
||||
}
|
16
src/Controller/FeedController.php
Normal file
16
src/Controller/FeedController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Entity;
|
||||
|
||||
use App\Repository\BucketRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
|
@ -28,12 +30,18 @@ class Bucket
|
|||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=Feed::class, mappedBy="bucket")
|
||||
*/
|
||||
private $feeds;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $slug
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->slug = $slug;
|
||||
$this->feeds = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
|
@ -50,4 +58,12 @@ class Bucket
|
|||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|Feed[]
|
||||
*/
|
||||
public function getFeeds(): Collection
|
||||
{
|
||||
return $this->feeds;
|
||||
}
|
||||
}
|
||||
|
|
89
src/Entity/Feed.php
Normal file
89
src/Entity/Feed.php
Normal 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;
|
||||
}
|
||||
}
|
37
src/Migrations/Version20200526192352.php
Normal file
37
src/Migrations/Version20200526192352.php
Normal 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');
|
||||
}
|
||||
}
|
50
src/Repository/FeedRepository.php
Normal file
50
src/Repository/FeedRepository.php
Normal 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()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
130
src/Service/OpmlImporter.php
Normal file
130
src/Service/OpmlImporter.php
Normal 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
38
src/Service/Slugger.php
Normal 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;
|
||||
}
|
||||
}
|
27
symfony.lock
27
symfony.lock
|
@ -132,6 +132,9 @@
|
|||
"symfony/asset": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
"symfony/browser-kit": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
"symfony/cache": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
|
@ -154,12 +157,18 @@
|
|||
"config/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"symfony/css-selector": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
"symfony/dependency-injection": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
"symfony/doctrine-bridge": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
"symfony/dom-crawler": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
"symfony/dotenv": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
|
@ -231,6 +240,21 @@
|
|||
"symfony/orm-pack": {
|
||||
"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": {
|
||||
"version": "v1.17.0"
|
||||
},
|
||||
|
@ -272,6 +296,9 @@
|
|||
"symfony/string": {
|
||||
"version": "v5.0.8"
|
||||
},
|
||||
"symfony/test-pack": {
|
||||
"version": "v1.0.6"
|
||||
},
|
||||
"symfony/translation-contracts": {
|
||||
"version": "v2.0.1"
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
||||
{% block stylesheets %}
|
||||
<link href="{{ asset('css/style.css') }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Hello BucketController!{% endblock %}
|
||||
{% block title %}{{ bucket.name }} | {{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<header>
|
||||
<h1>Bucket: {{ bucket.name }}</h1>
|
||||
<h1>Bucket: {{ bucket.name }} <small>with {{ bucket.feeds | length }} feeds</small></h1>
|
||||
</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 %}
|
||||
|
|
2
templates/feed/entries.html.twig
Normal file
2
templates/feed/entries.html.twig
Normal 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>
|
20
templates/feed/index.html.twig
Normal file
20
templates/feed/index.html.twig
Normal 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 %}
|
26
templates/feed/show.html.twig
Normal file
26
templates/feed/show.html.twig
Normal 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 %}
|
73
tests/Service/OpmlImporterTest.php
Normal file
73
tests/Service/OpmlImporterTest.php
Normal 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
11
tests/bootstrap.php
Normal 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');
|
||||
}
|
Loading…
Reference in a new issue