Skip to content
Snippets Groups Projects
Commit 2224026f authored by Tobias Gross-Vogt's avatar Tobias Gross-Vogt
Browse files

initial commit

parents
No related branches found
No related tags found
No related merge requests found
Pipeline #85964 passed
Showing
with 653 additions and 0 deletions
/vendor
/var
/.php_cs
/.idea
/*.cache
/_coverage
\ No newline at end of file
image: registry.gitlab.tugraz.at/dbp/relay/dbp-relay-core-bundle/main:v1
before_script:
- 'git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.tugraz.at/".insteadOf "git@gitlab.tugraz.at:"'
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/_composer_cache"
cache:
key: ${CI_PROJECT_PATH}
paths:
- _composer_cache
stages:
- test
.test_defaults: &test_defaults
script:
- sudo update-alternatives --set php "/usr/bin/${PHP}"
- composer install
- composer test
test-php7.3:
stage: test
variables:
PHP: "php7.3"
<<: *test_defaults
test-php7.4:
stage: test
variables:
PHP: "php7.4"
<<: *test_defaults
test-php8.0:
stage: test
variables:
PHP: "php8.0"
<<: *test_defaults
linting:
stage: test
script:
- sudo update-alternatives --set php /usr/bin/php7.4
- composer install
- result=0
- composer run cs || result=1
- composer run phpstan || result=1
- composer run psalm || result=1
- exit $result
\ No newline at end of file
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('var')
;
$config = new PhpCsFixer\Config();
$config->setRules([
'@Symfony' => true,
'@PHP70Migration' => true,
'@PHP71Migration' => true,
'@PHP73Migration' => true,
'@DoctrineAnnotation' => true,
'doctrine_annotation_array_assignment' => ['operator' => '='],
'yoda_style' => false,
'strict_comparison' => true,
'strict_param' => true,
'declare_strict_types' => true,
'method_argument_space' => ['on_multiline' => 'ignore'],
])
->setRiskyAllowed(true)
->setFinder($finder);
return $config;
\ No newline at end of file
{
"extends": [
"config:base",
"group:allNonMajor",
"schedule:weekends",
":automergePatch"
],
"ignorePresets": [":prHourlyLimit2"],
"rangeStrategy": "update-lockfile",
"lockFileMaintenance": {
"enabled": true
},
"packageRules": [
{
"matchPackagePrefixes": [
"symfony/"
],
"allowedVersions": "<6"
}
]
}
\ No newline at end of file
This diff is collapsed.
# DbpRelayCourseBundle
This Symfony bundle can be used as a template for creating new bundles for the
DBP Relay project.
When including this bundle into your API server it will gain the following
features:
* A custom `./bin/console` command
* An example entity
* Various HTTP methods implemented for that entity
## TL;DR
The quickest way to make use of this template bundle is to feed your desired names
to one command and generate a ready-to-use bundle with the correct naming.
See [Generate DBP Symfony bundle](https://dbp-demo.tugraz.at/dev-guide/relay/naming/#generate-dbp-symfony-bundle) for more information.
## Using the Bundle as a Template
* Copy the repo contents
* Adjust the package name in `composer.json`, in this example we'll pretend you named your bundle `dbp/relay-your-bundle`
* Invent a new PHP namespace and adjust it in all PHP files
* Rename `src/DbpRelayCourseBundle` and `DependencyInjection/DbpRelayCourseExtension` to match the new project name
## Integration into the API Server
* Push your bundle on a git server, in this example we'll use `git@gitlab.tugraz.at:dbp/relay/dbp-relay-your-bundle.git`
* Add the repository to your composer.json (as soon as you published your bundle to [Packagist](https://packagist.org/)
you can remove that block again):
```json
"repositories": [
{
"type": "vcs",
"url": "git@gitlab.tugraz.at:dbp/relay/dbp-relay-your-bundle.git"
}
],
```
* Add the bundle package as a dependency:
```bash
composer require dbp/relay-your-bundle=dev-main
```
* Add the bundle to your `config/bundles.php`:
```php
...
Dbp\Relay\YourBundle\DbpRelayYourBundle::class => ['all' => true],
DBP\API\CoreBundle\DbpCoreBundle::class => ['all' => true],
];
```
* Run `composer install` to clear caches
## Configuration
The bundle has a `example_config` configuration value that you can specify in your
app, either by hard-coding it, or by referencing an environment variable.
For this create `config/packages/dbp_relay_course.yaml` in the app with the following
content:
```yaml
dbp_relay_course:
example_config: 42
# example_config: '%env(EXAMPLE_CONFIG)%'
```
The value gets read in `DbpRelayCourseExtension` (your extension will be named differently)
and passed when creating the `MyCustomService` service.
For more info on bundle configuration see [Symfony bundles configuration](https://symfony.com/doc/current/bundles/configuration.html).
## Development & Testing
* Install dependencies: `composer install`
* Run tests: `composer test`
* Run linters: `composer run lint`
* Run cs-fixer: `composer run cs-fix`
## Bundle dependencies
Don't forget you need to pull down your dependencies in your main application if you are installing packages in a bundle.
```bash
# updates and installs dependencies from dbp/relay-your-bundle
composer update dbp/relay-your-bundle
```
{
"name": "dbp/relay-course-bundle",
"type": "symfony-bundle",
"license": "AGPL-3.0-or-later",
"require": {
"php": ">=7.3",
"ext-json": "*",
"api-platform/core": "^2.6",
"dbp/relay-core-bundle": "^0.1.11",
"symfony/framework-bundle": "^5.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"phpstan/phpstan": "^1.0.0",
"phpstan/phpstan-phpunit": "^1.0.0",
"phpunit/phpunit": "^9",
"symfony/browser-kit": "^5.4",
"symfony/http-client": "^5.4",
"symfony/monolog-bundle": "^3.7",
"symfony/phpunit-bridge": "^5.4",
"vimeo/psalm": "^4.2.1"
},
"autoload": {
"psr-4": {
"Dbp\\Relay\\CourseBundle\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Dbp\\Relay\\CourseBundle\\Tests\\": "tests/"
}
},
"config": {
"sort-packages": true,
"platform": {
"php": "7.3"
}
},
"scripts": {
"test": [
"@php vendor/bin/phpunit"
],
"phpstan": [
"@php vendor/bin/phpstan analyze --ansi"
],
"psalm": [
"@php vendor/bin/psalm"
],
"lint": [
"@composer run cs",
"@composer run phpstan",
"@composer run psalm"
],
"cs-fix": [
"@php vendor/bin/php-cs-fixer --ansi fix"
],
"cs": [
"@php vendor/bin/php-cs-fixer --ansi fix --dry-run --diff"
],
"coverage": [
"@php vendor/bin/phpunit --coverage-html _coverage"
]
}
}
This diff is collapsed.
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
parameters:
inferPrivatePropertyTypeFromConstructor: true
level: 3
paths:
- src
ignoreErrors:
- message: '#.*NodeDefinition::children.*#'
path: ./src/DependencyInjection
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php">
<coverage>
<include>
<directory>src</directory>
<directory>tests</directory>
</include>
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
<server name="APP_ENV" value="test" force="true"/>
<server name="SHELL_VERBOSITY" value="-1"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value='max[direct]=0&amp;quiet[]=indirect'/>
<server name="KERNEL_CLASS" value="Dbp\Relay\CourseBundle\Tests\Kernel"/>
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>
</phpunit>
<?xml version="1.0"?>
<psalm
totallyTyped="false"
errorLevel="5"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\Command;
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 TestCommand extends Command
{
protected static $defaultName = 'dbp:my-custom-command';
public function __construct()
{
parent::__construct();
}
protected function configure()
{
$this->addArgument('argument', InputArgument::REQUIRED, 'Example.');
$this->setDescription('Hey there!');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$argument = $input->getArgument('argument');
$output->writeln($argument);
return 0;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\Controller;
use Dbp\Relay\CourseBundle\Entity\Course;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
class LoggedInOnly extends AbstractController
{
public function __invoke(Course $data, Request $request): Course
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
return $data;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use Dbp\Relay\CourseBundle\Entity\Course;
use Dbp\Relay\CourseBundle\Service\CourseProviderInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class CourseDataPersister extends AbstractController implements DataPersisterInterface
{
private $api;
public function __construct(CourseProviderInterface $api)
{
$this->api = $api;
}
public function supports($data): bool
{
return $data instanceof Course;
}
public function persist($data): void
{
// TODO
}
public function remove($data)
{
// TODO
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\DataProvider;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use Dbp\Relay\CoreBundle\Helpers\ArrayFullPaginator;
use Dbp\Relay\CourseBundle\Entity\Course;
use Dbp\Relay\CourseBundle\Service\CourseProviderInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
final class CourseCollectionDataProvider extends AbstractController implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
private $api;
public function __construct(CourseProviderInterface $api)
{
$this->api = $api;
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Course::class === $resourceClass;
}
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): ArrayFullPaginator
{
$perPage = 30;
$page = 1;
$filters = $context['filters'] ?? [];
if (isset($filters['page'])) {
$page = (int) $filters['page'];
}
if (isset($filters['perPage'])) {
$perPage = (int) $filters['perPage'];
}
return new ArrayFullPaginator($this->api->getCourses(), $page, $perPage);
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\DataProvider;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use Dbp\Relay\CourseBundle\Entity\Course;
use Dbp\Relay\CourseBundle\Service\CourseProviderInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
final class CourseItemDataProvider extends AbstractController implements ItemDataProviderInterface, RestrictedDataProviderInterface
{
private $api;
public function __construct(CourseProviderInterface $api)
{
$this->api = $api;
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Course::class === $resourceClass;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?Course
{
return $this->api->getCourseById($id);
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class DbpRelayCourseBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('dbp_relay_course');
$treeBuilder->getRootNode()
->children()
->scalarNode('example_config')
->defaultValue('42')
->end()
->end();
return $treeBuilder;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
class DbpRelayCourseExtension extends ConfigurableExtension
{
public function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
$this->extendArrayParameter(
$container, 'api_platform.resource_class_directories', [__DIR__.'/../Entity']);
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yaml');
// Inject the config value into the MyCustomService service
$definition = $container->getDefinition('Dbp\Relay\CourseBundle\Service\MyCustomService');
$definition->addArgument($mergedConfig['example_config']);
}
private function extendArrayParameter(ContainerBuilder $container, string $parameter, array $values)
{
if (!$container->hasParameter($parameter)) {
$container->setParameter($parameter, []);
}
$oldValues = $container->getParameter($parameter);
assert(is_array($oldValues));
$container->setParameter($parameter, array_merge($oldValues, $values));
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CourseBundle\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Dbp\Relay\CourseBundle\Controller\LoggedInOnly;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ApiResource(
* collectionOperations={
* "get" = {
* "path" = "/course/courses",
* "openapi_context" = {
* "tags" = {"Course API"},
* },
* }
* },
* itemOperations={
* "get" = {
* "path" = "/course/courses/{identifier}",
* "openapi_context" = {
* "tags" = {"Course API"},
* },
* },
* "put" = {
* "path" = "/course/courses/{identifier}",
* "openapi_context" = {
* "tags" = {"Course API"},
* },
* },
* "delete" = {
* "path" = "/course/courses/{identifier}",
* "openapi_context" = {
* "tags" = {"Course API"},
* },
* },
* "loggedin_only" = {
* "security" = "is_granted('IS_AUTHENTICATED_FULLY')",
* "method" = "GET",
* "path" = "/course/courses/{identifier}/loggedin-only",
* "controller" = LoggedInOnly::class,
* "openapi_context" = {
* "summary" = "Only works when logged in.",
* "tags" = {"Course API"},
* },
* }
* },
* iri="https://schema.org/Course",
* shortName="CourseCourse",
* normalizationContext={
* "groups" = {"CourseCourse:output"},
* "jsonld_embed_context" = true
* },
* denormalizationContext={
* "groups" = {"CourseCourse:input"},
* "jsonld_embed_context" = true
* }
* )
*/
class Course
{
/**
* @ApiProperty(identifier=true)
*/
private $identifier;
/**
* @ApiProperty(iri="https://schema.org/name")
* @Groups({"CourseCourse:output", "CourseCourse:input"})
*
* @var string
*/
private $name;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function setIdentifier(string $identifier): void
{
$this->identifier = $identifier;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment