diff --git a/.env b/.env
index d17fcc5..3c8c7c0 100644
--- a/.env
+++ b/.env
@@ -39,3 +39,7 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###> symfony/mailer ###
# MAILER_DSN=null://null
###< symfony/mailer ###
+
+###> nelmio/cors-bundle ###
+CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
+###< nelmio/cors-bundle ###
diff --git a/README.md b/README.md
index 870cbb9..f125209 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# matsen-tool-be
-Installation:
+# Installation:
gehe ins root Verzeichnis des Projekts und für aus: ddev config
@@ -10,24 +10,78 @@ Installation:
projekt installieren: ddev composer install
-Troubleshooting:
+# Troubleshooting:
Unable to listen on required ports, port 443 is already in use
-> setze andere Ports in .ddev/config.yaml z.B.
router_http_port: 8080
router_https_port: 8443
-PHPMyAdmin installieren:
+ - https://stackoverflow.com/questions/76507208/symfony-6-3-migration-causes-problems-with-stateless-authenticators-forcing-requ
+
+# PHPMyAdmin installieren:
ddev get ddev/ddev-phpmyadmin
ddev restart
-Symfony Konsolenbefehle mit Ddev ausführen, z.B.:
+# Symfony Konsolenbefehle mit Ddev ausführen, z.B.:
ddev exec php bin/console make:migration
-Ddev Commands:
+# Ddev Commands:
ddev describe - zeigt Urls und installierte Komponenten
+-------------------------
+# Symfony:
+
+# User with Maker Bundle:
+ - https://symfonycasts.com/screencast/api-platform/user-entity
+ ddev composer require maker-bundle --dev
+ ddev exec bin/console make:user -> erstellt user entity und schreibt in die security.yaml
+
+# Entity erzeugen oder erweitern:
+ ddev exec bin/console make:entity
+
+# Foundry fixtures:
+ ddev exec composer require foundry orm-fixtures --dev
+ ddev exec bin/console make:factory
+ ddev exec bin/console doctrine:fixtures:load
+
+# Doctrine:
+ ddev exec bin/console doctrine:database:drop --force
+ ddev exec bin/console doctrine:database:create
+ ddev exec bin/console make:migration
+ ddev exec bin/console doctrine:migration:migrate
+
+# Profiler
+ ddev composer require debug
+
+# Php Unit
+ - https://symfony.com/doc/current/testing.html#configuring-a-database-for-tests
+ Setup:
+ # .env.test.local -> "mysql://root:root@db:3306/db?serverVersion=10.4.30-MariaDB-1:10.4.30+maria~ubu2004-log - mariadb.org binary distribution"
+ -> this creates a db named db_test (it takes the name of the main database "db" and adds "_test" to its name)
+ # Create db and create schema
+ php bin/console --env=test doctrine:database:create
+ php bin/console --env=test doctrine:schema:create
+
+ - https://symfonycasts.com/screencast/api-platform-security/test-setup
+ ddev composer require test -> testpack incl. phpunit
+ ddev composer require zenstruck/browser --dev -> browser test package to imporve testing
+ -> add extension to phpunit.xml.dist
+
+
+
+ ddev exec php bin/phpunit --filter=testPostToCreateNewUserPost
+
+ ddev composer require --dev mtdowling/jmespath.php
+
+
+# Api Platform
+ - https://api-platform.com/docs/core/dto/
+ - https://api-platform.com/docs/distribution/
+ - https://api-platform.com/docs/core/extending/
+ ddev exec bin/console api:openapi:export --yaml
+ -> export OpenApi spec
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 6e98ce7..fa47b67 100644
--- a/composer.json
+++ b/composer.json
@@ -7,9 +7,11 @@
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
+ "api-platform/core": "^3.2",
"doctrine/doctrine-bundle": "^2.11",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^2.17",
+ "nelmio/cors-bundle": "^2.4",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.24",
"symfony/asset": "7.0.*",
@@ -38,6 +40,7 @@
"symfony/validator": "7.0.*",
"symfony/web-link": "7.0.*",
"symfony/yaml": "7.0.*",
+ "symfonycasts/micro-mapper": "^0.1.4",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
@@ -91,13 +94,17 @@
}
},
"require-dev": {
+ "doctrine/doctrine-fixtures-bundle": "^3.5",
+ "mtdowling/jmespath.php": "^2.7",
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "7.0.*",
"symfony/css-selector": "7.0.*",
"symfony/debug-bundle": "7.0.*",
- "symfony/maker-bundle": "^1.0",
+ "symfony/maker-bundle": "^1.52",
"symfony/phpunit-bridge": "^7.0",
"symfony/stopwatch": "7.0.*",
- "symfony/web-profiler-bundle": "7.0.*"
+ "symfony/web-profiler-bundle": "7.0.*",
+ "zenstruck/browser": "^1.6",
+ "zenstruck/foundry": "^1.36"
}
}
diff --git a/composer.lock b/composer.lock
index a4d13dd..1b33cc4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,171 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "3bc207441cd949595a265e045a50c919",
+ "content-hash": "e29fe980d3968cb83139b26407957151",
"packages": [
+ {
+ "name": "api-platform/core",
+ "version": "v3.2.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/core.git",
+ "reference": "f297d2192652a3acd2a644707740de8cb5069221"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/core/zipball/f297d2192652a3acd2a644707740de8cb5069221",
+ "reference": "f297d2192652a3acd2a644707740de8cb5069221",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/inflector": "^1.0 || ^2.0",
+ "php": ">=8.1",
+ "psr/cache": "^1.0 || ^2.0 || ^3.0",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/deprecation-contracts": "^3.1",
+ "symfony/http-foundation": "^6.1 || ^7.0",
+ "symfony/http-kernel": "^6.1 || ^7.0",
+ "symfony/property-access": "^6.1 || ^7.0",
+ "symfony/property-info": "^6.1 || ^7.0",
+ "symfony/serializer": "^6.1 || ^7.0",
+ "symfony/translation-contracts": "^3.3",
+ "symfony/web-link": "^6.1 || ^7.0",
+ "willdurand/negotiation": "^3.0"
+ },
+ "conflict": {
+ "doctrine/common": "<3.2.2",
+ "doctrine/dbal": "<2.10",
+ "doctrine/mongodb-odm": "<2.4",
+ "doctrine/orm": "<2.14.0",
+ "doctrine/persistence": "<1.3",
+ "elasticsearch/elasticsearch": ">=8.0,<8.4",
+ "phpspec/prophecy": "<1.15",
+ "phpunit/phpunit": "<9.5",
+ "symfony/var-exporter": "<6.1.1"
+ },
+ "require-dev": {
+ "behat/behat": "^3.11",
+ "behat/mink": "^1.9",
+ "doctrine/cache": "^1.11 || ^2.1",
+ "doctrine/common": "^3.2.2",
+ "doctrine/dbal": "^3.4.0",
+ "doctrine/doctrine-bundle": "^1.12 || ^2.0",
+ "doctrine/mongodb-odm": "^2.2",
+ "doctrine/orm": "^2.14",
+ "elasticsearch/elasticsearch": "^7.11 || ^8.4",
+ "friends-of-behat/mink-browserkit-driver": "^1.3.1",
+ "friends-of-behat/mink-extension": "^2.2",
+ "friends-of-behat/symfony-extension": "^2.1",
+ "guzzlehttp/guzzle": "^6.0 || ^7.0",
+ "jangregor/phpstan-prophecy": "^1.0",
+ "justinrainbow/json-schema": "^5.2.1",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpdoc-parser": "^1.13",
+ "phpstan/phpstan": "^1.1",
+ "phpstan/phpstan-doctrine": "^1.0",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpstan/phpstan-symfony": "^1.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "ramsey/uuid": "^3.9.7 || ^4.0",
+ "ramsey/uuid-doctrine": "^1.4 || ^2.0",
+ "soyuka/contexts": "v3.3.9",
+ "soyuka/stubs-mongodb": "^1.0",
+ "symfony/asset": "^6.1 || ^7.0",
+ "symfony/browser-kit": "^6.1 || ^7.0",
+ "symfony/cache": "^6.1 || ^7.0",
+ "symfony/config": "^6.1 || ^7.0",
+ "symfony/console": "^6.1 || ^7.0",
+ "symfony/css-selector": "^6.1 || ^7.0",
+ "symfony/dependency-injection": "^6.1 || ^7.0.12",
+ "symfony/doctrine-bridge": "^6.1 || ^7.0",
+ "symfony/dom-crawler": "^6.1 || ^7.0",
+ "symfony/error-handler": "^6.1 || ^7.0",
+ "symfony/event-dispatcher": "^6.1 || ^7.0",
+ "symfony/expression-language": "^6.1 || ^7.0",
+ "symfony/finder": "^6.1 || ^7.0",
+ "symfony/form": "^6.1 || ^7.0",
+ "symfony/framework-bundle": "^6.1 || ^7.0",
+ "symfony/http-client": "^6.1 || ^7.0",
+ "symfony/intl": "^6.1 || ^7.0",
+ "symfony/maker-bundle": "^1.24",
+ "symfony/mercure-bundle": "*",
+ "symfony/messenger": "^6.1 || ^7.0",
+ "symfony/phpunit-bridge": "^6.1 || ^7.0",
+ "symfony/routing": "^6.1 || ^7.0",
+ "symfony/security-bundle": "^6.1 || ^7.0",
+ "symfony/security-core": "^6.1 || ^7.0",
+ "symfony/stopwatch": "^6.1 || ^7.0",
+ "symfony/twig-bundle": "^6.1 || ^7.0",
+ "symfony/uid": "^6.1 || ^7.0",
+ "symfony/validator": "^6.1 || ^7.0",
+ "symfony/web-profiler-bundle": "^6.1 || ^7.0",
+ "symfony/yaml": "^6.1 || ^7.0",
+ "twig/twig": "^1.42.3 || ^2.12 || ^3.0",
+ "webonyx/graphql-php": "^14.0 || ^15.0"
+ },
+ "suggest": {
+ "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
+ "elasticsearch/elasticsearch": "To support Elasticsearch.",
+ "ocramius/package-versions": "To display the API Platform's version in the debug bar.",
+ "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
+ "psr/cache-implementation": "To use metadata caching.",
+ "ramsey/uuid": "To support Ramsey's UUID identifiers.",
+ "symfony/cache": "To have metadata caching when using Symfony integration.",
+ "symfony/config": "To load XML configuration files.",
+ "symfony/expression-language": "To use authorization features.",
+ "symfony/http-client": "To use the HTTP cache invalidation system.",
+ "symfony/messenger": "To support messenger integration.",
+ "symfony/security": "To use authorization features.",
+ "symfony/twig-bundle": "To use the Swagger UI integration.",
+ "symfony/uid": "To support Symfony UUID/ULID identifiers.",
+ "symfony/web-profiler-bundle": "To use the data collector.",
+ "webonyx/graphql-php": "To support GraphQL."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.3.x-dev"
+ },
+ "symfony": {
+ "require": "^6.1 || ^7.0"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ }
+ ],
+ "description": "Build a fully-featured hypermedia or GraphQL API in minutes!",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "hal",
+ "jsonapi",
+ "openapi",
+ "rest",
+ "swagger"
+ ],
+ "support": {
+ "issues": "https://github.com/api-platform/core/issues",
+ "source": "https://github.com/api-platform/core/tree/v3.2.7"
+ },
+ "time": "2023-11-30T13:51:25+00:00"
+ },
{
"name": "doctrine/cache",
"version": "2.2.0",
@@ -1497,6 +1660,68 @@
],
"time": "2023-10-27T15:32:31+00:00"
},
+ {
+ "name": "nelmio/cors-bundle",
+ "version": "2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nelmio/NelmioCorsBundle.git",
+ "reference": "78fcdb91f76b080a1008133def9c7f613833933d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/78fcdb91f76b080a1008133def9c7f613833933d",
+ "reference": "78fcdb91f76b080a1008133def9c7f613833933d",
+ "shasum": ""
+ },
+ "require": {
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.3.6",
+ "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Nelmio\\CorsBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nelmio",
+ "homepage": "http://nelm.io"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors"
+ }
+ ],
+ "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application",
+ "keywords": [
+ "api",
+ "cors",
+ "crossdomain"
+ ],
+ "support": {
+ "issues": "https://github.com/nelmio/NelmioCorsBundle/issues",
+ "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.4.0"
+ },
+ "time": "2023-11-30T16:41:19+00:00"
+ },
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
@@ -7019,6 +7244,55 @@
],
"time": "2023-11-07T10:26:03+00:00"
},
+ {
+ "name": "symfonycasts/micro-mapper",
+ "version": "v0.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SymfonyCasts/micro-mapper.git",
+ "reference": "81190a2c94359213afc3053c5dd85c9fd2c3d7bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SymfonyCasts/micro-mapper/zipball/81190a2c94359213afc3053c5dd85c9fd2c3d7bb",
+ "reference": "81190a2c94359213afc3053c5dd85c9fd2c3d7bb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.10.39",
+ "symfony/filesystem": "^6.3",
+ "symfony/framework-bundle": "^6.3",
+ "symfony/phpunit-bridge": "^6.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfonycasts\\MicroMapper\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ryan Weaver",
+ "homepage": "https://symfonycasts.com"
+ }
+ ],
+ "description": "A tiny, underwhelming data mapper to map one object to another!",
+ "keywords": [
+ "data-mapper"
+ ],
+ "support": {
+ "issues": "https://github.com/SymfonyCasts/micro-mapper/issues",
+ "source": "https://github.com/SymfonyCasts/micro-mapper/tree/v0.1.4"
+ },
+ "time": "2023-10-18T15:25:09+00:00"
+ },
{
"name": "twig/extra-bundle",
"version": "v3.8.0",
@@ -7222,39 +7496,36 @@
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
},
"time": "2022-06-03T18:03:27+00:00"
- }
- ],
- "packages-dev": [
+ },
{
- "name": "masterminds/html5",
- "version": "2.8.1",
+ "name": "willdurand/negotiation",
+ "version": "3.1.0",
"source": {
"type": "git",
- "url": "https://github.com/Masterminds/html5-php.git",
- "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf"
+ "url": "https://github.com/willdurand/Negotiation.git",
+ "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf",
- "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf",
+ "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2",
+ "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2",
"shasum": ""
},
"require": {
- "ext-dom": "*",
- "php": ">=5.3.0"
+ "php": ">=7.1.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8"
+ "symfony/phpunit-bridge": "^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
- "Masterminds\\": "src"
+ "Negotiation\\": "src/Negotiation"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -7263,149 +7534,579 @@
],
"authors": [
{
- "name": "Matt Butcher",
- "email": "technosophos@gmail.com"
- },
- {
- "name": "Matt Farina",
- "email": "matt@mattfarina.com"
- },
- {
- "name": "Asmir Mustafic",
- "email": "goetas@gmail.com"
+ "name": "William Durand",
+ "email": "will+git@drnd.me"
}
],
- "description": "An HTML5 parser and serializer.",
- "homepage": "http://masterminds.github.io/html5-php",
+ "description": "Content Negotiation tools for PHP provided as a standalone library.",
+ "homepage": "http://williamdurand.fr/Negotiation/",
"keywords": [
- "HTML5",
- "dom",
- "html",
- "parser",
- "querypath",
- "serializer",
- "xml"
+ "accept",
+ "content",
+ "format",
+ "header",
+ "negotiation"
],
"support": {
- "issues": "https://github.com/Masterminds/html5-php/issues",
- "source": "https://github.com/Masterminds/html5-php/tree/2.8.1"
+ "issues": "https://github.com/willdurand/Negotiation/issues",
+ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0"
},
- "time": "2023-05-10T11:58:31+00:00"
- },
+ "time": "2022-01-30T20:08:53+00:00"
+ }
+ ],
+ "packages-dev": [
{
- "name": "myclabs/deep-copy",
- "version": "1.11.1",
+ "name": "behat/mink",
+ "version": "v1.11.0",
"source": {
"type": "git",
- "url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ "url": "https://github.com/minkphp/Mink.git",
+ "reference": "d8527fdf8785aad38455fb426af457ab9937aece"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
- "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "url": "https://api.github.com/repos/minkphp/Mink/zipball/d8527fdf8785aad38455fb426af457ab9937aece",
+ "reference": "d8527fdf8785aad38455fb426af457ab9937aece",
"shasum": ""
},
"require": {
- "php": "^7.1 || ^8.0"
- },
- "conflict": {
- "doctrine/collections": "<1.6.8",
- "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ "php": ">=7.2",
+ "symfony/css-selector": "^4.4 || ^5.0 || ^6.0 || ^7.0"
},
"require-dev": {
- "doctrine/collections": "^1.6.8",
- "doctrine/common": "^2.13.3 || ^3.2.2",
- "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^8.5.22 || ^9.5.11",
+ "symfony/error-handler": "^4.4 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0"
+ },
+ "suggest": {
+ "behat/mink-browserkit-driver": "fast headless driver for any app without JS emulation",
+ "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)",
+ "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)",
+ "dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
"autoload": {
- "files": [
- "src/DeepCopy/deep_copy.php"
- ],
"psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
+ "Behat\\Mink\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "description": "Create deep copies (clones) of your objects",
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Browser controller/emulator abstraction for PHP",
+ "homepage": "https://mink.behat.org/",
"keywords": [
- "clone",
- "copy",
- "duplicate",
- "object",
- "object graph"
+ "browser",
+ "testing",
+ "web"
],
"support": {
- "issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ "issues": "https://github.com/minkphp/Mink/issues",
+ "source": "https://github.com/minkphp/Mink/tree/v1.11.0"
},
- "funding": [
- {
- "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
- "type": "tidelift"
- }
- ],
- "time": "2023-03-08T13:26:56+00:00"
+ "time": "2023-12-09T11:23:23+00:00"
},
{
- "name": "nikic/php-parser",
- "version": "v4.17.1",
+ "name": "doctrine/data-fixtures",
+ "version": "1.7.0",
"source": {
"type": "git",
- "url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d"
+ "url": "https://github.com/doctrine/data-fixtures.git",
+ "reference": "bbcb74f2ac6dbe81a14b3c3687d7623490a0448f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
- "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
+ "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/bbcb74f2ac6dbe81a14b3c3687d7623490a0448f",
+ "reference": "bbcb74f2ac6dbe81a14b3c3687d7623490a0448f",
"shasum": ""
},
"require": {
- "ext-tokenizer": "*",
- "php": ">=7.0"
+ "doctrine/deprecations": "^0.5.3 || ^1.0",
+ "doctrine/persistence": "^2.0|^3.0",
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "<3.5 || >=5",
+ "doctrine/orm": "<2.14 || >=4",
+ "doctrine/phpcr-odm": "<1.3.0"
},
"require-dev": {
- "ircmaxell/php-yacc": "^0.0.7",
- "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ "doctrine/annotations": "^1.12 || ^2",
+ "doctrine/coding-standard": "^12",
+ "doctrine/dbal": "^3.5 || ^4",
+ "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0",
+ "doctrine/orm": "^2.14 || ^3",
+ "ext-sqlite3": "*",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.6.13 || ^10.4.2",
+ "symfony/cache": "^5.4 || ^6.3 || ^7",
+ "symfony/var-exporter": "^5.4 || ^6.3 || ^7",
+ "vimeo/psalm": "^5.9"
},
- "bin": [
- "bin/php-parse"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.9-dev"
- }
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)",
+ "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures",
+ "doctrine/orm": "For loading ORM fixtures",
+ "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures"
},
+ "type": "library",
"autoload": {
"psr-4": {
- "PhpParser\\": "lib/PhpParser"
+ "Doctrine\\Common\\DataFixtures\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Nikita Popov"
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
}
],
- "description": "A PHP parser written in PHP",
+ "description": "Data Fixtures for all Doctrine Object Managers",
+ "homepage": "https://www.doctrine-project.org",
"keywords": [
- "parser",
- "php"
+ "database"
],
"support": {
- "issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1"
+ "issues": "https://github.com/doctrine/data-fixtures/issues",
+ "source": "https://github.com/doctrine/data-fixtures/tree/1.7.0"
},
- "time": "2023-08-13T19:53:39+00:00"
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-11-24T11:18:31+00:00"
+ },
+ {
+ "name": "doctrine/doctrine-fixtures-bundle",
+ "version": "3.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/DoctrineFixturesBundle.git",
+ "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/c808a0c85c38c8ee265cc8405b456c1d2b38567d",
+ "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/data-fixtures": "^1.3",
+ "doctrine/doctrine-bundle": "^2.2",
+ "doctrine/orm": "^2.14.0 || ^3.0",
+ "doctrine/persistence": "^2.4|^3.0",
+ "php": "^7.4 || ^8.0",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/doctrine-bridge": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "< 3"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^12",
+ "phpstan/phpstan": "^1.10.39",
+ "phpunit/phpunit": "^9.6.13",
+ "symfony/phpunit-bridge": "^6.3.6",
+ "vimeo/psalm": "^5.15"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Bundle\\FixturesBundle\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Doctrine Project",
+ "homepage": "https://www.doctrine-project.org"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony DoctrineFixturesBundle",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "Fixture",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues",
+ "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.5.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-11-19T12:48:54+00:00"
+ },
+ {
+ "name": "fakerphp/faker",
+ "version": "v1.23.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FakerPHP/Faker.git",
+ "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
+ "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "conflict": {
+ "fzaninotto/faker": "*"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.4.1",
+ "doctrine/persistence": "^1.3 || ^2.0",
+ "ext-intl": "*",
+ "phpunit/phpunit": "^9.5.26",
+ "symfony/phpunit-bridge": "^5.4.16"
+ },
+ "suggest": {
+ "doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
+ "ext-curl": "Required by Faker\\Provider\\Image to download images.",
+ "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
+ "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
+ "ext-mbstring": "Required for multibyte Unicode string functionality."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "v1.21-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Faker\\": "src/Faker/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "François Zaninotto"
+ }
+ ],
+ "description": "Faker is a PHP library that generates fake data for you.",
+ "keywords": [
+ "data",
+ "faker",
+ "fixtures"
+ ],
+ "support": {
+ "issues": "https://github.com/FakerPHP/Faker/issues",
+ "source": "https://github.com/FakerPHP/Faker/tree/v1.23.0"
+ },
+ "time": "2023-06-12T08:44:38+00:00"
+ },
+ {
+ "name": "masterminds/html5",
+ "version": "2.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Masterminds/html5-php.git",
+ "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf",
+ "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Masterminds\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matt Butcher",
+ "email": "technosophos@gmail.com"
+ },
+ {
+ "name": "Matt Farina",
+ "email": "matt@mattfarina.com"
+ },
+ {
+ "name": "Asmir Mustafic",
+ "email": "goetas@gmail.com"
+ }
+ ],
+ "description": "An HTML5 parser and serializer.",
+ "homepage": "http://masterminds.github.io/html5-php",
+ "keywords": [
+ "HTML5",
+ "dom",
+ "html",
+ "parser",
+ "querypath",
+ "serializer",
+ "xml"
+ ],
+ "support": {
+ "issues": "https://github.com/Masterminds/html5-php/issues",
+ "source": "https://github.com/Masterminds/html5-php/tree/2.8.1"
+ },
+ "time": "2023-05-10T11:58:31+00:00"
+ },
+ {
+ "name": "mtdowling/jmespath.php",
+ "version": "2.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jmespath/jmespath.php.git",
+ "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b",
+ "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "symfony/polyfill-mbstring": "^1.17"
+ },
+ "require-dev": {
+ "composer/xdebug-handler": "^3.0.3",
+ "phpunit/phpunit": "^8.5.33"
+ },
+ "bin": [
+ "bin/jp.php"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/JmesPath.php"
+ ],
+ "psr-4": {
+ "JmesPath\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Declaratively specify how to extract elements from a JSON document",
+ "keywords": [
+ "json",
+ "jsonpath"
+ ],
+ "support": {
+ "issues": "https://github.com/jmespath/jmespath.php/issues",
+ "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0"
+ },
+ "time": "2023-08-25T10:54:48+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-03-08T13:26:56+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.17.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
+ "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1"
+ },
+ "time": "2023-08-13T19:53:39+00:00"
},
{
"name": "phar-io/manifest",
@@ -9481,6 +10182,285 @@
}
],
"time": "2023-11-20T00:12:19+00:00"
+ },
+ {
+ "name": "zenstruck/assert",
+ "version": "v1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zenstruck/assert.git",
+ "reference": "60956bb6584a51c6c2ab9fa8707b7c013d770163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zenstruck/assert/zipball/60956bb6584a51c6c2ab9fa8707b7c013d770163",
+ "reference": "60956bb6584a51c6c2ab9fa8707b7c013d770163",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0",
+ "symfony/polyfill-php81": "^1.23",
+ "symfony/var-exporter": "^5.4|^6.0|^7.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.4",
+ "phpunit/phpunit": "^9.5",
+ "symfony/phpunit-bridge": "^6.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Zenstruck\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kevin Bond",
+ "email": "kevinbond@gmail.com"
+ }
+ ],
+ "description": "Standalone, lightweight, framework agnostic, test assertion library.",
+ "homepage": "https://github.com/zenstruck/assert",
+ "keywords": [
+ "assertion",
+ "phpunit",
+ "test"
+ ],
+ "support": {
+ "issues": "https://github.com/zenstruck/assert/issues",
+ "source": "https://github.com/zenstruck/assert/tree/v1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kbond",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-02T09:08:04+00:00"
+ },
+ {
+ "name": "zenstruck/browser",
+ "version": "v1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zenstruck/browser.git",
+ "reference": "9e931f5d5a62007f6642bec47992f54574ebc5f1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zenstruck/browser/zipball/9e931f5d5a62007f6642bec47992f54574ebc5f1",
+ "reference": "9e931f5d5a62007f6642bec47992f54574ebc5f1",
+ "shasum": ""
+ },
+ "require": {
+ "behat/mink": "^1.8",
+ "php": ">=8.0",
+ "symfony/browser-kit": "^5.4|^6.0|^7.0",
+ "symfony/css-selector": "^5.4|^6.0|^7.0",
+ "symfony/dom-crawler": "^5.4|^6.0|^7.0",
+ "symfony/framework-bundle": "^5.4|^6.0|^7.0",
+ "zenstruck/assert": "^1.1",
+ "zenstruck/callback": "^1.4.2"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "^1.0",
+ "justinrainbow/json-schema": "^5.2",
+ "mtdowling/jmespath.php": "^2.6",
+ "phpstan/phpstan": "^1.4",
+ "phpunit/phpunit": "^9.5",
+ "symfony/mime": "^5.4|^6.0|^7.0",
+ "symfony/panther": "^1.1|^2.0.1",
+ "symfony/phpunit-bridge": "^6.0|^7.0",
+ "symfony/security-bundle": "^5.4|^6.0|^7.0",
+ "zenstruck/foundry": "^1.30"
+ },
+ "suggest": {
+ "justinrainbow/json-schema": "Json schema validator. Needed to use Json::assertMatchesSchema().",
+ "mtdowling/jmespath.php": "PHP implementation for JMESPath. Needed to use Json assertions."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Zenstruck\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kevin Bond",
+ "email": "kevinbond@gmail.com"
+ }
+ ],
+ "description": "A fluent interface for your Symfony functional tests.",
+ "homepage": "https://github.com/zenstruck/browser",
+ "keywords": [
+ "symfony",
+ "test"
+ ],
+ "support": {
+ "issues": "https://github.com/zenstruck/browser/issues",
+ "source": "https://github.com/zenstruck/browser/tree/v1.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kbond",
+ "type": "github"
+ }
+ ],
+ "time": "2023-10-31T17:14:32+00:00"
+ },
+ {
+ "name": "zenstruck/callback",
+ "version": "v1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zenstruck/callback.git",
+ "reference": "eed9a532fd8974368e60c4a2550ed65eab7e5432"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zenstruck/callback/zipball/eed9a532fd8974368e60c4a2550ed65eab7e5432",
+ "reference": "eed9a532fd8974368e60c4a2550ed65eab7e5432",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.14"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Zenstruck\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kevin Bond",
+ "email": "kevinbond@gmail.com"
+ }
+ ],
+ "description": "Callable wrapper to validate and inject arguments.",
+ "homepage": "https://github.com/zenstruck/callback",
+ "keywords": [
+ "callable",
+ "callback",
+ "utility"
+ ],
+ "support": {
+ "issues": "https://github.com/zenstruck/callback/issues",
+ "source": "https://github.com/zenstruck/callback/tree/v1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kbond",
+ "type": "github"
+ }
+ ],
+ "time": "2022-08-31T14:56:15+00:00"
+ },
+ {
+ "name": "zenstruck/foundry",
+ "version": "v1.36.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zenstruck/foundry.git",
+ "reference": "1aefc394059a315ef8567de9745efd5ba6aacfd0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zenstruck/foundry/zipball/1aefc394059a315ef8567de9745efd5ba6aacfd0",
+ "reference": "1aefc394059a315ef8567de9745efd5ba6aacfd0",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/persistence": "^1.3.3|^2.0|^3.0",
+ "fakerphp/faker": "^1.10",
+ "php": ">=8.0",
+ "symfony/deprecation-contracts": "^2.2|^3.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/string": "^5.4|^6.0|^7.0",
+ "zenstruck/assert": "^1.0",
+ "zenstruck/callback": "^1.1"
+ },
+ "conflict": {
+ "doctrine/mongodb-odm": "2.5.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.4",
+ "dama/doctrine-test-bundle": "^7.0",
+ "doctrine/doctrine-bundle": "^2.5",
+ "doctrine/doctrine-migrations-bundle": "^2.2|^3.0",
+ "doctrine/mongodb-odm": "^2.0",
+ "doctrine/mongodb-odm-bundle": "^4.4.0",
+ "doctrine/orm": "^2.9",
+ "matthiasnoback/symfony-dependency-injection-test": "^4.1",
+ "symfony/framework-bundle": "^5.4|^6.0|^7.0",
+ "symfony/maker-bundle": "^1.49",
+ "symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
+ "symfony/translation-contracts": "^2.5|^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "target-directory": "bin/tools",
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Zenstruck\\Foundry\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kevin Bond",
+ "email": "kevinbond@gmail.com"
+ }
+ ],
+ "description": "A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.",
+ "homepage": "https://github.com/zenstruck/foundry",
+ "keywords": [
+ "Fixture",
+ "dev",
+ "doctrine",
+ "factory",
+ "faker",
+ "symfony",
+ "test"
+ ],
+ "support": {
+ "issues": "https://github.com/zenstruck/foundry/issues",
+ "source": "https://github.com/zenstruck/foundry/tree/v1.36.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kbond",
+ "type": "github"
+ }
+ ],
+ "time": "2023-10-13T18:44:57+00:00"
}
],
"aliases": [],
diff --git a/config/bundles.php b/config/bundles.php
index 770ad3c..eaa8354 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -11,4 +11,9 @@ return [
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
+ Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
+ ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
+ Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
+ Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true],
+ Symfonycasts\MicroMapper\SymfonycastsMicroMapperBundle::class => ['all' => true],
];
diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml
new file mode 100644
index 0000000..47fc8f5
--- /dev/null
+++ b/config/packages/api_platform.yaml
@@ -0,0 +1,21 @@
+api_platform:
+ title: Matsen API Platform
+ version: 1.0.0
+ formats:
+ jsonld: [ 'application/ld+json' ]
+ json: [ 'application/json' ]
+ html: [ 'text/html' ]
+ jsonhal: [ 'application/hal+json' ]
+ docs_formats:
+ jsonld: ['application/ld+json']
+ jsonopenapi: ['application/vnd.openapi+json']
+ html: ['text/html']
+ defaults:
+ stateless: true
+ cache_headers:
+ vary: ['Content-Type', 'Authorization', 'Origin']
+ extra_properties:
+ standard_put: true
+ rfc_7807_compliant_errors: true
+ event_listeners_backward_compatibility_layer: false
+ keep_legacy_inflector: false
diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml
new file mode 100644
index 0000000..c766508
--- /dev/null
+++ b/config/packages/nelmio_cors.yaml
@@ -0,0 +1,10 @@
+nelmio_cors:
+ defaults:
+ origin_regex: true
+ allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
+ allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
+ allow_headers: ['Content-Type', 'Authorization']
+ expose_headers: ['Link']
+ max_age: 3600
+ paths:
+ '^/': null
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index 367af25..dcb8fc1 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -4,14 +4,22 @@ security:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
- users_in_memory: { memory: null }
+ # used to reload user from session & other features (e.g. switch_user)
+ app_user_provider:
+ entity:
+ class: App\Entity\User
+ property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
- provider: users_in_memory
+ provider: app_user_provider
+ json_login:
+ check_path: app_login
+ username_path: email
+ password_path: password
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
diff --git a/config/packages/zenstruck_foundry.yaml b/config/packages/zenstruck_foundry.yaml
new file mode 100644
index 0000000..0657d2c
--- /dev/null
+++ b/config/packages/zenstruck_foundry.yaml
@@ -0,0 +1,7 @@
+when@dev: &dev
+ # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
+ zenstruck_foundry:
+ # Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh)
+ auto_refresh_proxies: true
+
+when@test: *dev
diff --git a/config/routes.yaml b/config/routes.yaml
index 41ef814..7c25751 100644
--- a/config/routes.yaml
+++ b/config/routes.yaml
@@ -3,3 +3,4 @@ controllers:
path: ../src/Controller/
namespace: App\Controller
type: attribute
+ stateless: false
\ No newline at end of file
diff --git a/config/routes/api_platform.yaml b/config/routes/api_platform.yaml
new file mode 100644
index 0000000..35cafe3
--- /dev/null
+++ b/config/routes/api_platform.yaml
@@ -0,0 +1,5 @@
+api_platform:
+ resource: .
+ type: api_platform
+ prefix: /api
+ stateless: false
diff --git a/migrations/Version20231214140601.php b/migrations/Version20231214140601.php
new file mode 100644
index 0000000..581efd6
--- /dev/null
+++ b/migrations/Version20231214140601.php
@@ -0,0 +1,35 @@
+addSql('CREATE TABLE posting (id INT AUTO_INCREMENT NOT NULL, owner_id INT NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_BD275D737E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, roles JSON NOT NULL COMMENT \'(DC2Type:json)\', password VARCHAR(255) NOT NULL, active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('ALTER TABLE posting ADD CONSTRAINT FK_BD275D737E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE posting DROP FOREIGN KEY FK_BD275D737E3C61F9');
+ $this->addSql('DROP TABLE posting');
+ $this->addSql('DROP TABLE `user`');
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6c4bfed..1063dcf 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -34,5 +34,6 @@
+
diff --git a/src/ApiResource/.gitignore b/src/ApiResource/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/src/ApiResource/PostingApi.php b/src/ApiResource/PostingApi.php
new file mode 100644
index 0000000..e6ac88f
--- /dev/null
+++ b/src/ApiResource/PostingApi.php
@@ -0,0 +1,60 @@
+
+ * @date 12.12.23
+ */
+
+
+namespace App\ApiResource;
+
+use ApiPlatform\Doctrine\Orm\State\Options;
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use App\Entity\Posting;
+use App\State\EntityClassDtoStateProcessor;
+use App\State\EntityToDtoStateProvider;
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Patch;
+use ApiPlatform\Metadata\Post;
+use App\Validator\IsValidOwner;
+use Symfony\Component\Validator\Constraints\NotBlank;
+
+#[ApiResource(
+ shortName: 'Post',
+ operations: [
+ new Get(
+ security: 'is_granted("ROLE_USER")'
+ ),
+ new GetCollection(),
+ new Post(
+ security: 'is_granted("ROLE_USER")',
+ ),
+ new Patch(
+ security: 'is_granted("EDIT", object)',
+ ),
+ new Delete(
+ security: 'is_granted("ROLE_ADMIN")',
+ )
+ ],
+ paginationItemsPerPage: 10,
+ security: 'is_granted("ROLE_USER")',
+ provider: EntityToDtoStateProvider::class,
+ processor: EntityClassDtoStateProcessor::class,
+ stateOptions: new Options(entityClass: Posting::class),
+)]
+class PostingApi
+{
+ #[ApiProperty(readable: false, writable: false, identifier: true)]
+ public ?int $id = null;
+
+ #[NotBlank]
+ public ?string $message = null;
+
+ #[IsValidOwner]
+ public ?UserApi $owner = null;
+
+ #[ApiProperty(writable: false)]
+ public ?\DateTimeImmutable $createdAt = null;
+}
\ No newline at end of file
diff --git a/src/ApiResource/UserApi.php b/src/ApiResource/UserApi.php
new file mode 100644
index 0000000..774ca98
--- /dev/null
+++ b/src/ApiResource/UserApi.php
@@ -0,0 +1,82 @@
+
+ * @date 12.12.23
+ */
+
+
+namespace App\ApiResource;
+
+use ApiPlatform\Doctrine\Orm\State\Options;
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Patch;
+use ApiPlatform\Metadata\Post;
+use App\Entity\User;
+use App\State\EntityClassDtoStateProcessor;
+use App\State\EntityToDtoStateProvider;
+use Symfony\Component\Validator\Constraints as Assert;
+
+#[ApiResource(
+ shortName: 'User',
+ operations: [
+ new Get(
+ security: 'is_granted("ROLE_USER")'
+ ),
+ new GetCollection(
+ security: 'is_granted("ROLE_USER")'
+ ),
+ new Post(
+ security: 'is_granted("PUBLIC_ACCESS")',
+ validationContext: ['groups' => ['Default', 'postValidation']],
+ ),
+ new Patch(
+ security: 'is_granted("ROLE_USER")'
+ ),
+ ],
+ paginationItemsPerPage: 10,
+ security: 'is_granted("ROLE_USER")',
+ provider: EntityToDtoStateProvider::class,
+ processor: EntityClassDtoStateProcessor::class,
+ stateOptions: new Options(entityClass: User::class),
+
+)]
+class UserApi
+{
+ #[ApiProperty(readable: false, writable: false, identifier: true)]
+ public ?int $id = null;
+
+ #[Assert\NotBlank]
+ #[Assert\Email]
+ public ?string $email = null;
+
+ #[Assert\NotBlank]
+ public ?string $firstName = null;
+
+ #[Assert\NotBlank]
+ public ?string $lastName = null;
+
+ /**
+ * The plaintext password when being set or changed.
+ */
+ #[ApiProperty(readable: false)]
+ #[Assert\NotBlank(groups: ['postValidation'])]
+ public ?string $password = null;
+
+ // Object is null ONLY during deserialization: so this allows isPublished
+ // to be writable in ALL cases (which is ok because the operations are secured).
+ // During serialization, object will always be a DragonTreasureApi, so our
+ // voter is called.
+ #[ApiProperty(security: 'object === null or is_granted("EDIT", object)')]
+ public bool $active;
+
+ #[ApiProperty(writable: false)]
+ public ?\DateTimeImmutable $createdAt = null;
+
+ /**
+ * @var array
+ */
+ public array $userPosts = [];
+}
\ No newline at end of file
diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php
new file mode 100644
index 0000000..c3d0934
--- /dev/null
+++ b/src/Controller/SecurityController.php
@@ -0,0 +1,33 @@
+json([
+ 'error' => 'Invalid login request: check that the Content-Type header is "application/json".',
+ ], 401);
+ }
+
+ return new Response(null, 204, [
+ 'Location' => $iriConverter->getIriFromResource($user),
+ ]);
+ }
+
+ #[Route('/logout', name: 'app_logout')]
+ public function logout(): void
+ {
+ throw new \Exception('This should never be reached!');
+ }
+}
diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php
new file mode 100644
index 0000000..f2bf156
--- /dev/null
+++ b/src/DataFixtures/AppFixtures.php
@@ -0,0 +1,41 @@
+ 'd.knudsen@spawntree.de',
+ 'firstName' => 'Daniel',
+ 'lastName' => 'Knudsen',
+ 'password' => 'test',
+ ]
+ );
+ $adminD->setRoles(['ROLE_ADMIN']);
+
+ $adminF = UserFactory::createOne(
+ [
+ 'email' => 'f.eisenmenger@spawntree.de',
+ 'firstName' => 'Florian',
+ 'lastName' => 'Eisenmenger',
+ 'password' => 'test',
+ ]
+ );
+ $adminF->setRoles(['ROLE_ADMIN']);
+
+ UserFactory::createMany(10);
+ PostingFactory::createMany(50, function() {
+ return [
+ 'owner' => UserFactory::random()
+ ];
+ });
+ }
+}
diff --git a/src/Entity/Posting.php b/src/Entity/Posting.php
new file mode 100644
index 0000000..29d27e4
--- /dev/null
+++ b/src/Entity/Posting.php
@@ -0,0 +1,65 @@
+createdAt = new \DateTimeImmutable();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getMessage(): ?string
+ {
+ return $this->message;
+ }
+
+ public function setMessage(string $message): static
+ {
+ $this->message = $message;
+
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function getOwner(): ?User
+ {
+ return $this->owner;
+ }
+
+ public function setOwner(?User $owner): static
+ {
+ $this->owner = $owner;
+
+ return $this;
+ }
+}
diff --git a/src/Entity/User.php b/src/Entity/User.php
new file mode 100644
index 0000000..a24f35c
--- /dev/null
+++ b/src/Entity/User.php
@@ -0,0 +1,196 @@
+createdAt = new \DateTimeImmutable();
+ $this->userPosts = new ArrayCollection();
+ $this->active = true;
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getEmail(): ?string
+ {
+ return $this->email;
+ }
+
+ public function setEmail(string $email): static
+ {
+ $this->email = $email;
+
+ return $this;
+ }
+
+ /**
+ * A visual identifier that represents this user.
+ *
+ * @see UserInterface
+ */
+ public function getUserIdentifier(): string
+ {
+ return (string) $this->email;
+ }
+
+ /**
+ * @see UserInterface
+ */
+ public function getRoles(): array
+ {
+ $roles = $this->roles;
+ // guarantee every user at least has ROLE_USER
+ $roles[] = 'ROLE_USER';
+
+ return array_unique($roles);
+ }
+
+ public function setRoles(array $roles): static
+ {
+ $this->roles = $roles;
+
+ return $this;
+ }
+
+ /**
+ * @see PasswordAuthenticatedUserInterface
+ */
+ public function getPassword(): string
+ {
+ return $this->password;
+ }
+
+ public function setPassword(string $password): static
+ {
+ $this->password = $password;
+
+ return $this;
+ }
+
+ /**
+ * @see UserInterface
+ */
+ public function eraseCredentials(): void
+ {
+ // If you store any temporary, sensitive data on the user, clear it here
+ // $this->plainPassword = null;
+ }
+
+ public function getFirstName(): ?string
+ {
+ return $this->firstName;
+ }
+
+ public function setFirstName(string $firstName): static
+ {
+ $this->firstName = $firstName;
+
+ return $this;
+ }
+
+ public function getLastName(): ?string
+ {
+ return $this->lastName;
+ }
+
+ public function setLastName(string $lastName): static
+ {
+ $this->lastName = $lastName;
+
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getUserPosts(): Collection
+ {
+ return $this->userPosts;
+ }
+
+ public function addUserPost(Posting $post): static
+ {
+ if (!$this->userPosts->contains($post)) {
+ $this->userPosts->add($post);
+ $post->setOwner($this);
+ }
+
+ return $this;
+ }
+
+ public function removeUserPost(Posting $post): static
+ {
+ if ($this->userPosts->removeElement($post)) {
+ // set the owning side to null (unless already changed)
+ if ($post->getOwner() === $this) {
+ $post->setOwner(null);
+ }
+ }
+
+ return $this;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->active;
+ }
+
+ public function setActive(bool $active): static
+ {
+ $this->active = $active;
+
+ return $this;
+ }
+}
diff --git a/src/Factory/PostingFactory.php b/src/Factory/PostingFactory.php
new file mode 100644
index 0000000..c73838e
--- /dev/null
+++ b/src/Factory/PostingFactory.php
@@ -0,0 +1,69 @@
+
+ *
+ * @method Posting|Proxy create(array|callable $attributes = [])
+ * @method static Posting|Proxy createOne(array $attributes = [])
+ * @method static Posting|Proxy find(object|array|mixed $criteria)
+ * @method static Posting|Proxy findOrCreate(array $attributes)
+ * @method static Posting|Proxy first(string $sortedField = 'id')
+ * @method static Posting|Proxy last(string $sortedField = 'id')
+ * @method static Posting|Proxy random(array $attributes = [])
+ * @method static Posting|Proxy randomOrCreate(array $attributes = [])
+ * @method static PostingRepository|RepositoryProxy repository()
+ * @method static Posting[]|Proxy[] all()
+ * @method static Posting[]|Proxy[] createMany(int $number, array|callable $attributes = [])
+ * @method static Posting[]|Proxy[] createSequence(iterable|callable $sequence)
+ * @method static Posting[]|Proxy[] findBy(array $attributes)
+ * @method static Posting[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
+ * @method static Posting[]|Proxy[] randomSet(int $number, array $attributes = [])
+ */
+final class PostingFactory extends ModelFactory
+{
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
+ *
+ * @todo inject services if required
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
+ *
+ * @todo add your default values here
+ */
+ protected function getDefaults(): array
+ {
+ return [
+ 'message' => self::faker()->text(),
+ 'owner' => UserFactory::new(),
+ ];
+ }
+
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
+ */
+ protected function initialize(): self
+ {
+ return $this
+ // ->afterInstantiate(function(Post $post): void {})
+ ;
+ }
+
+ protected static function getClass(): string
+ {
+ return Posting::class;
+ }
+}
diff --git a/src/Factory/UserFactory.php b/src/Factory/UserFactory.php
new file mode 100644
index 0000000..77aea1e
--- /dev/null
+++ b/src/Factory/UserFactory.php
@@ -0,0 +1,94 @@
+
+ *
+ * @method User|Proxy create(array|callable $attributes = [])
+ * @method static User|Proxy createOne(array $attributes = [])
+ * @method static User|Proxy find(object|array|mixed $criteria)
+ * @method static User|Proxy findOrCreate(array $attributes)
+ * @method static User|Proxy first(string $sortedField = 'id')
+ * @method static User|Proxy last(string $sortedField = 'id')
+ * @method static User|Proxy random(array $attributes = [])
+ * @method static User|Proxy randomOrCreate(array $attributes = [])
+ * @method static UserRepository|RepositoryProxy repository()
+ * @method static User[]|Proxy[] all()
+ * @method static User[]|Proxy[] createMany(int $number, array|callable $attributes = [])
+ * @method static User[]|Proxy[] createSequence(iterable|callable $sequence)
+ * @method static User[]|Proxy[] findBy(array $attributes)
+ * @method static User[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
+ * @method static User[]|Proxy[] randomSet(int $number, array $attributes = [])
+ */
+final class UserFactory extends ModelFactory
+{
+ const FIRST_NAMES = [
+ 'Alice', 'Bob', 'Charlie', 'David', 'Emma',
+ 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack',
+ 'Kate', 'Liam', 'Mia', 'Noah', 'Olivia',
+ 'Paul', 'Quinn', 'Ryan', 'Sophia', 'Thomas',
+ 'Ursula', 'Victor', 'Wendy', 'Xander', 'Yvonne'
+ ];
+
+ const LAST_NAMES = [
+ 'Smith', 'Johnson', 'Williams', 'Jones', 'Brown',
+ 'Davis', 'Miller', 'Wilson', 'Moore', 'Taylor',
+ 'Anderson', 'Thomas', 'Jackson', 'White', 'Harris',
+ 'Martin', 'Thompson', 'Garcia', 'Martinez', 'Robinson',
+ 'Clark', 'Lewis', 'Lee', 'Walker', 'Hall'
+ ];
+
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
+ *
+ * @todo inject services if required
+ */
+ public function __construct(private UserPasswordHasherInterface $passwordHasher)
+ {
+ parent::__construct();
+ }
+
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
+ *
+ * @todo add your default values here
+ */
+ protected function getDefaults(): array
+ {
+ return [
+ 'email' => self::faker()->email(),
+ 'firstName' => self::faker()->randomElement(self::FIRST_NAMES),
+ 'lastName' => self::faker()->randomElement(self::LAST_NAMES),
+ 'password' => "test",
+ 'roles' => [],
+ ];
+ }
+
+ /**
+ * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
+ */
+ protected function initialize(): self
+ {
+ return $this
+ ->afterInstantiate(function(User $user): void {
+ $user->setPassword($this->passwordHasher->hashPassword(
+ $user,
+ $user->getPassword()
+ ));
+ })
+ ;
+ }
+
+ protected static function getClass(): string
+ {
+ return User::class;
+ }
+}
diff --git a/src/Mapper/PostingApiToEntityMapper.php b/src/Mapper/PostingApiToEntityMapper.php
new file mode 100644
index 0000000..a7f4cf0
--- /dev/null
+++ b/src/Mapper/PostingApiToEntityMapper.php
@@ -0,0 +1,61 @@
+id ? $this->repository->find($dto->id) : new Posting();
+ if (!$entity) {
+ throw new \Exception('DragonTreasure not found');
+ }
+
+ return $entity;
+ }
+
+ public function populate(object $from, object $to, array $context): object
+ {
+ $dto = $from;
+ $entity = $to;
+ assert($dto instanceof PostingApi);
+ assert($entity instanceof Posting);
+
+ if ($dto->owner) {
+ $entity->setOwner($this->microMapper->map($dto->owner, User::class, [
+ MicroMapperInterface::MAX_DEPTH => 0,
+ ]));
+ } else {
+ $entity->setOwner($this->security->getUser());
+ }
+
+ $entity->setMessage($dto->message);
+
+ return $entity;
+ }
+}
diff --git a/src/Mapper/PostingEntityToApiMapper.php b/src/Mapper/PostingEntityToApiMapper.php
new file mode 100644
index 0000000..52fcb41
--- /dev/null
+++ b/src/Mapper/PostingEntityToApiMapper.php
@@ -0,0 +1,51 @@
+id = $entity->getId();
+
+ return $dto;
+ }
+
+ public function populate(object $from, object $to, array $context): object
+ {
+ $entity = $from;
+ $dto = $to;
+ assert($entity instanceof Posting);
+ assert($dto instanceof PostingApi);
+
+ $dto->message = $entity->getMessage();
+ $dto->owner = $this->microMapper->map($entity->getOwner(), UserApi::class, [
+ MicroMapperInterface::MAX_DEPTH => 0,
+ ]);
+ $dto->createdAt = $entity->getCreatedAt();
+
+ return $dto;
+ }
+}
diff --git a/src/Mapper/UserApiToEntityMapper.php b/src/Mapper/UserApiToEntityMapper.php
new file mode 100644
index 0000000..d218d94
--- /dev/null
+++ b/src/Mapper/UserApiToEntityMapper.php
@@ -0,0 +1,65 @@
+id ? $this->userRepository->find($dto->id) : new User();
+ if (!$userEntity) {
+ throw new \Exception('User not found');
+ }
+
+ return $userEntity;
+ }
+
+ public function populate(object $from, object $to, array $context): object
+ {
+ $dto = $from;
+ assert($dto instanceof UserApi);
+ $entity = $to;
+ assert($entity instanceof User);
+
+ $entity->setEmail($dto->email);
+ $entity->setFirstName($dto->firstName);
+ $entity->setLastName($dto->lastName);
+ if ($dto->password) {
+ $entity->setPassword($this->userPasswordHasher->hashPassword($entity, $dto->password));
+ }
+
+ $userPostsEntities = [];
+ foreach ($dto->userPosts as $userPostApi) {
+ $userPostsEntities[] = $this->microMapper->map($userPostApi, Posting::class, [
+ MicroMapperInterface::MAX_DEPTH => 0,
+ ]);
+ }
+ $this->propertyAccessor->setValue($entity, 'userPosts', $userPostsEntities);
+
+ return $entity;
+ }
+}
diff --git a/src/Mapper/UserEntityToApiMapper.php b/src/Mapper/UserEntityToApiMapper.php
new file mode 100644
index 0000000..ccf7086
--- /dev/null
+++ b/src/Mapper/UserEntityToApiMapper.php
@@ -0,0 +1,51 @@
+id = $entity->getId();
+
+ return $dto;
+ }
+
+ public function populate(object $from, object $to, array $context): object
+ {
+ $entity = $from;
+ $dto = $to;
+ assert($entity instanceof User);
+ assert($dto instanceof UserApi);
+
+ $dto->email = $entity->getEmail();
+ $dto->firstName = $entity->getFirstName();
+ $dto->lastName = $entity->getLastName();
+ $dto->userPosts = array_map(function(Posting $userPost) {
+ return $this->microMapper->map($userPost, PostingApi::class, [
+ MicroMapperInterface::MAX_DEPTH => 0,
+ ]);
+ }, $entity->getUserPosts()->getValues());
+
+ return $dto;
+ }
+}
diff --git a/src/Repository/PostingRepository.php b/src/Repository/PostingRepository.php
new file mode 100644
index 0000000..a56b69f
--- /dev/null
+++ b/src/Repository/PostingRepository.php
@@ -0,0 +1,48 @@
+
+ *
+ * @method Posting|null find($id, $lockMode = null, $lockVersion = null)
+ * @method Posting|null findOneBy(array $criteria, array $orderBy = null)
+ * @method Posting[] findAll()
+ * @method Posting[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class PostingRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Posting::class);
+ }
+
+// /**
+// * @return Post[] Returns an array of Post objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('p')
+// ->andWhere('p.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('p.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?Post
+// {
+// return $this->createQueryBuilder('p')
+// ->andWhere('p.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php
new file mode 100644
index 0000000..c788f46
--- /dev/null
+++ b/src/Repository/UserRepository.php
@@ -0,0 +1,67 @@
+
+ *
+ * @implements PasswordUpgraderInterface
+ *
+ * @method User|null find($id, $lockMode = null, $lockVersion = null)
+ * @method User|null findOneBy(array $criteria, array $orderBy = null)
+ * @method User[] findAll()
+ * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, User::class);
+ }
+
+ /**
+ * Used to upgrade (rehash) the user's password automatically over time.
+ */
+ public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
+ {
+ if (!$user instanceof User) {
+ throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
+ }
+
+ $user->setPassword($newHashedPassword);
+ $this->getEntityManager()->persist($user);
+ $this->getEntityManager()->flush();
+ }
+
+// /**
+// * @return User[] Returns an array of User objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('u')
+// ->andWhere('u.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('u.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?User
+// {
+// return $this->createQueryBuilder('u')
+// ->andWhere('u.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/src/State/EntityClassDtoStateProcessor.php b/src/State/EntityClassDtoStateProcessor.php
new file mode 100644
index 0000000..b53846f
--- /dev/null
+++ b/src/State/EntityClassDtoStateProcessor.php
@@ -0,0 +1,49 @@
+getStateOptions();
+ assert($stateOptions instanceof Options);
+ $entityClass = $stateOptions->getEntityClass();
+
+ $entity = $this->mapDtoToEntity($data, $entityClass);
+
+ if ($operation instanceof DeleteOperationInterface) {
+ $this->removeProcessor->process($entity, $operation, $uriVariables, $context);
+
+ return null;
+ }
+
+ $this->persistProcessor->process($entity, $operation, $uriVariables, $context);
+ $data->id = $entity->getId();
+
+ return $data;
+ }
+
+ private function mapDtoToEntity(object $dto, string $entityClass): object
+ {
+ return $this->microMapper->map($dto, $entityClass);
+ }
+}
diff --git a/src/State/EntityToDtoStateProvider.php b/src/State/EntityToDtoStateProvider.php
new file mode 100644
index 0000000..de489de
--- /dev/null
+++ b/src/State/EntityToDtoStateProvider.php
@@ -0,0 +1,59 @@
+getClass();
+ if ($operation instanceof CollectionOperationInterface) {
+ $entities = $this->collectionProvider->provide($operation, $uriVariables, $context);
+ assert($entities instanceof Paginator);
+
+ $dtos = [];
+ foreach ($entities as $entity) {
+ $dtos[] = $this->mapEntityToDto($entity, $resourceClass);
+ }
+
+ return new TraversablePaginator(
+ new \ArrayIterator($dtos),
+ $entities->getCurrentPage(),
+ $entities->getItemsPerPage(),
+ $entities->getTotalItems()
+ );
+ }
+
+ $entity = $this->itemProvider->provide($operation, $uriVariables, $context);
+
+ if (!$entity) {
+ return null;
+ }
+
+ return $this->mapEntityToDto($entity, $resourceClass);
+ }
+
+ private function mapEntityToDto(object $entity, string $resourceClass): object
+ {
+ return $this->microMapper->map($entity, $resourceClass);
+ }
+}
diff --git a/src/Validator/IsValidOwner.php b/src/Validator/IsValidOwner.php
new file mode 100644
index 0000000..034ceda
--- /dev/null
+++ b/src/Validator/IsValidOwner.php
@@ -0,0 +1,20 @@
+security->getUser();
+ if (!$user) {
+ throw new \LogicException('IsOwnerValidator should only be used when a user is logged in.');
+ }
+ assert($user instanceof User);
+
+ if ($value->id !== $user->getId()) {
+ $this->context->buildViolation($constraint->message)
+ ->addViolation();
+ }
+ }
+}
diff --git a/src/Voter/PostingApiVoter.php b/src/Voter/PostingApiVoter.php
new file mode 100644
index 0000000..00a880d
--- /dev/null
+++ b/src/Voter/PostingApiVoter.php
@@ -0,0 +1,49 @@
+getUser();
+ // if the user is anonymous, do not grant access
+ if (!$user instanceof User) {
+ return false;
+ }
+
+ if ($this->security->isGranted('ROLE_ADMIN')) {
+ return true;
+ }
+
+ assert($subject instanceof PostingApi);
+
+ // ... (check conditions and return true to grant permission) ...
+ switch ($attribute) {
+ case self::EDIT:
+ if ($subject->owner?->id === $user->getId()) {
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+}
diff --git a/src/Voter/UserApiVoter.php b/src/Voter/UserApiVoter.php
new file mode 100644
index 0000000..7325600
--- /dev/null
+++ b/src/Voter/UserApiVoter.php
@@ -0,0 +1,50 @@
+getUser();
+ // if the user is anonymous, do not grant access
+ if (!$user instanceof User) {
+ return false;
+ }
+
+ if ($this->security->isGranted('ROLE_ADMIN')) {
+ return true;
+ }
+
+ assert($subject instanceof UserApi);
+
+ // ... (check conditions and return true to grant permission) ...
+ switch ($attribute) {
+ case self::EDIT:
+ if ($subject->id === $user->getId()) {
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+}
diff --git a/symfony.lock b/symfony.lock
index 48250b2..0c12af2 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -1,4 +1,18 @@
{
+ "api-platform/core": {
+ "version": "3.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.2",
+ "ref": "696d44adc3c0d4f5d25a2f1c4f3700dd8a5c6db9"
+ },
+ "files": [
+ "config/packages/api_platform.yaml",
+ "config/routes/api_platform.yaml",
+ "src/ApiResource/.gitignore"
+ ]
+ },
"doctrine/doctrine-bundle": {
"version": "2.11",
"recipe": {
@@ -13,6 +27,18 @@
"src/Repository/.gitignore"
]
},
+ "doctrine/doctrine-fixtures-bundle": {
+ "version": "3.5",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
+ },
+ "files": [
+ "src/DataFixtures/AppFixtures.php"
+ ]
+ },
"doctrine/doctrine-migrations-bundle": {
"version": "3.3",
"recipe": {
@@ -26,6 +52,18 @@
"migrations/.gitignore"
]
},
+ "nelmio/cors-bundle": {
+ "version": "2.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.5",
+ "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
+ },
+ "files": [
+ "config/packages/nelmio_cors.yaml"
+ ]
+ },
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
@@ -244,7 +282,22 @@
"config/routes/web_profiler.yaml"
]
},
+ "symfonycasts/micro-mapper": {
+ "version": "v0.1.4"
+ },
"twig/extra-bundle": {
"version": "v3.8.0"
+ },
+ "zenstruck/foundry": {
+ "version": "1.36",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.10",
+ "ref": "37c2f894cc098ab4c08874b80cccc8e2f8de7976"
+ },
+ "files": [
+ "config/packages/zenstruck_foundry.yaml"
+ ]
}
}
diff --git a/tests/Functional/UserResourceTest.php b/tests/Functional/UserResourceTest.php
new file mode 100644
index 0000000..d65e387
--- /dev/null
+++ b/tests/Functional/UserResourceTest.php
@@ -0,0 +1,114 @@
+
+ * @date 12.12.23
+ */
+
+
+namespace App\Tests\Functional;
+
+use App\Factory\UserFactory;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Zenstruck\Browser\Json;
+use Zenstruck\Browser\Test\HasBrowser;
+use Zenstruck\Foundry\Test\Factories;
+use Zenstruck\Foundry\Test\ResetDatabase;
+
+class UserResourceTest extends KernelTestCase
+{
+ use HasBrowser;
+ use ResetDatabase;
+ use Factories;
+
+ public function testPostToCreateUser(): void
+ {
+ $this->browser()
+ ->post('/api/users', [
+ 'json' => [
+ 'email' => 'draggin_in_the_morning@coffee.com',
+ 'firstName' => 'Danny',
+ 'lastName' => 'Boy',
+ 'password' => 'password',
+ ]
+ ])
+ ->assertStatus(201)
+ ->use(function (Json $json) {
+ $json->assertMissing('password');
+ $json->assertMissing('id');
+ })
+ ->post('/login', [
+ 'json' => [
+ 'email' => 'draggin_in_the_morning@coffee.com',
+ 'password' => 'password',
+ ]
+ ])
+ ->assertSuccessful()
+ ;
+ }
+
+ public function testGetUsersWithoutAuthentication()
+ {
+ UserFactory::createMany(2);
+ $this->browser()
+ ->get('/api/users')
+ ->assertStatus(401)
+ ;
+ }
+
+ public function testGetOneUserWithoutAuthentication()
+ {
+ UserFactory::createOne();
+ $this->browser()
+ ->get('/api/users/1')
+ ->assertStatus(401)
+ ;
+ }
+
+ public function testPatchUserAsSameUser()
+ {
+ $user = UserFactory::createOne(
+ [
+ 'firstName' => 'John',
+ 'lastName' => 'Doe'
+ ]
+ );
+
+ $this->browser()
+ ->actingAs($user)
+ ->patch('/api/users/' . $user->getId(), [
+ 'json' => [
+ 'firstName' => 'Joe',
+ 'lastName' => 'Black'
+ ],
+ 'headers' => ['Content-Type' => 'application/merge-patch+json']
+ ])
+ ->assertStatus(200)
+ ->get('/api/users/' . $user->getId())
+ ->assertStatus(200)
+ ->assertJsonMatches('firstName', 'Joe')
+ ->assertJsonMatches('lastName', 'Black')
+ ;
+ }
+
+ public function testPatchUserInactiveAsSameUser()
+ {
+ $user = UserFactory::createOne(
+ [
+ 'firstName' => 'John'
+ ]
+ );
+
+ $this->browser()
+ ->actingAs($user)
+ ->patch('/api/users/' . $user->getId(), [
+ 'json' => [
+ 'firstName' => 'Joe'
+ ],
+ 'headers' => ['Content-Type' => 'application/merge-patch+json']
+ ])
+ ->assertStatus(200)
+ ->get('/api/users/' . $user->getId())
+ ->assertJsonMatches('firstName', 'A shiny thing')
+ ;
+ }
+}
\ No newline at end of file