Appite – validace zanořené entity

Richard Faila
Člen | 40
+
0
-

Zdravím

Potřeboval jsem udělat api, tak jsem se rozhodl zkusit appite. Nemělo by to být nic složitého, pár endpoitů, ale bude muset zpracovávat a validovat velké json struktury.

Šel jsem podle dokumentace a zkusil udělat jednoduché vytvoření uživatele.

namespace App\Controllers;

use Apitte\Core\Annotation\Controller\Method;
use Apitte\Core\Annotation\Controller\Path;
use Apitte\Core\Annotation\Controller\Responses;
use Apitte\Core\Annotation\Controller\Response;
use Apitte\Core\Annotation\Controller\RequestBody;
use Apitte\Core\Annotation\Controller\Tag;
use Apitte\Core\Annotation\Controller\RequestParameter;
use Apitte\Core\Annotation\Controller\RequestParameters;
use Apitte\Core\Http\ApiRequest;
use Apitte\Core\Http\ApiResponse;
use App\Entity\Request\User;
use Apitte\Negotiation\Http\ArrayEntity;


/**
 * @Path("/user")
 */
final class UserController extends BaseV1Controller
{
	/**
	 * @Path("/create")
	 * @Method("POST")
	 * @RequestBody(entity="App\Entity\Request\User")
	 * @Responses({
	 *     @Response(code="200", description="Success"),
	 *     @Response(code="404", description="Not found")
	 * })
	 */
	public function create(ApiRequest $request, ApiResponse $response): ApiResponse
	{
		/** @var User $entity */
	    $entity = $request->getEntity();

		var_dump($entity);

		return $response
			->withStatus(ApiResponse::S200_OK)
			->withEntity(ArrayEntity::from($entity->toArray()));
	}

}

Vstup je

namespace App\Entity\Request;

use Apitte\Core\Mapping\Request\BasicEntity;
use Symfony\Component\Validator\Constraints as Assert;

final class User extends BasicEntity
{

    /**
     * @var string
     *
     * @Assert\NotBlank
     */
    public $name;

    /**
     * @var string
     *
     * @Assert\NotBlank
     */
    public $surname;

	/**
	 * @var Address
	 *
	 * @Assert\NotBlank
	 * @Assert\Type("App\Entity\Request\Address")
	 * @Assert\Valid
	 */
	public $address;
}

kde je zanořený objekt s adresou (samostatná třída pro další použití):

namespace App\Entity\Request;

use Apitte\Core\Mapping\Request\BasicEntity;
use Symfony\Component\Validator\Constraints as Assert;

final class Address extends BasicEntity
{
	/**
	 * @var string
	 * @Assert\NotBlank
	 */
	public $street;

	/**
	 * @var string
	 * @Assert\NotBlank
	 */
	public $city;
}

Jenže při pokusu o odeslání:

{
    "name": "John",
    "surname": "Doe",
    "address": {
        "street": "Browning street",
        "city": "London"
    }
}

Dostanu chybu:

{
    "status": "error",
    "data": {
        "code": 422,
        "error": "Request body contains an error. See context for details.",
        "context": {
            "validation": {
                "address": [
                    "This value should be of type App\\Entity\\Request\\Address."
                ]
            }
        }
    }
}

Pokud z adresy odeberu anotaci s typem, adresa se nevaliduje vůbec. Uniká mi něco? Dík moc.

Felix
Nette Core | 1245
+
0
-

To vypada jako vystup ze symfony/validator. Mohl bys napsat jake verze PHP a knihoven pouzivas?

A uplne idealni by bylo, nemohl bys pridat tuhle ukazku do skeletonu (https://github.com/…te-skeleton) Ja bych to pak mohl rovnou vyzkouset.

Richard Faila
Člen | 40
+
0
-

PHP 7.4, verze knihoven by měli být nejnovější, vše jsem stahoval přes composer require

{
	"name": "nette/web-project",
	"description": "Nette: Standard Web Project",
	"keywords": ["nette"],
	"type": "project",
	"license": ["MIT", "BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
	"require": {
		"php": ">= 7.4",
		"nette/application": "^3.1",
		"nette/bootstrap": "^3.1",
		"nette/caching": "^3.1",
		"nette/database": "^3.1",
		"nette/di": "^3.0",
		"nette/security": "^3.1",
		"nette/utils": "^3.2",
		"tracy/tracy": "^2.8",
		"contributte/apitte": "^0.9.1",
		"nettrine/annotations": "^0.7.0",
		"symfony/validator": "^5.4",
		"nettrine/cache": "^0.3.0",
		"contributte/validator": "^1.1"
	},
	"require-dev": {
		"nette/tester": "^2.3",
		"symfony/thanks": "^1"
	},
	"autoload": {
		"psr-4": {
			"App\\": "app"
		}
	},
	"minimum-stability": "stable",
	"config": {
		"allow-plugins": {
			"symfony/thanks": true
		}
	}
}

Na ten skeleton se podívám a pošlu odkaz na repo.

Richard Faila
Člen | 40
+
0
-

Konečně jsem se k tomu dostal:

Skeleton vyhazuje 500 a v syslogu je chybová hláška:

[2022-08-12T12:24:17.428305+02:00] default.ERROR: No request entity found {"exception":"[object] (Apitte\\Core\\Exception\\Logical\\InvalidStateException(code: 0): No request entity found at /home/jakub/Web/api/vendor/apitte/core/src/Http/ApiRequest.php:47)"} {"url":"/api/public/v1/user/create","ip":"::1","http_method":"POST","server":"localhost","referrer":null,"file":"/home/jakub/Web/api/vendor/apitte/core/src/ErrorHandler/PsrLogErrorHandler.php","line":34,"class":"Apitte\\Core\\ErrorHandler\\PsrLogErrorHandler","callType":"->","function":"handle","memory_peak_usage":"8 MB","process_id":6809}

Odkaz na repo: https://github.com/…tte-skeleton

Felix
Nette Core | 1245
+
0
-

Supr, zkusim se na to co nejdrive podivat.

Richard Faila
Člen | 40
+
0
-

Při testování samostatné komponenty Symfony/Serializer jsem přišel na konfiguraci, která data zpracuje tak jak potřebuji, tj, včetně správného otypování zanořených entit.

Je to stejné jako Serializer, který je použit ve skeletonu, ale je tam menší změna v konstruktoru ObjectNormalizer:

new ObjectNormalizer(
	new,
	new CamelCaseToSnakeCaseNameConverter(),
	null,
	new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()])
)

Problém je, že nevím jak nejlépe dostat Symfony/Serializer do apitte (aby zpracovávalo deserializaci entity), Apitte\Core\DI\Plugin\CoreMappingPlugin ho nepoužívá (na rozdíl od skeletonu) a v konfiguraci je jen možnost nastavit Validator. Chápu správně že budu muset vytvořit vlastní implementaci CoreMappingPlugin?

Richard Faila
Člen | 40
+
0
-

Ještě pro upřesnění, chyba je ve třídě Apitte\Core\Mapping\RequestEntityMapping pluginu CoreMappingPlugin, konkrétně v metodě createEntity. V současné podobě podle mě neumí vytvořit zanořenou entitu ani jinou třídu.

Je tu řešení, kdy se dá ve vaší entitě (v mém případě User) implementovat metoda normalize a podle jména následně rozlišit, kdy z příchozích dat (pole), vytvořit objekt (Address).

Problém je, že je to vázané na jméno (na typ to nejde, tuto informaci v této metodě nemáte) a pokud máte více entit, se kterými chcete v projektu pracovat, tak to musíte udělat všude.

Za mě by bylo asi nejlepší tam nějak dostat Symfony\Serialiazer (ideálně přes config jako validator), který tohle řeší a umí i další věci (DateTime, CamelCaseToSnakeCase, atd…).

Editoval Richard Faila (16. 8. 2022 9:49)

Richard Faila
Člen | 40
+
0
-

Nakonec jsem to vyřešil tak, jak jsem uváděl v https://forum.nette.org/…orene-entity#… tj. podědil jsem si CoreMappingPlugin a doplnil do něj podporu pro Symfony\Serialiazer, který se potom používá v metodě createEntity třídy Apitte\Core\Mapping\RequestEntityMapping kterou jsem musel taky podědit a přepsat.

kleinpeter
Člen | 7
+
0
-

Ahoj,
potýkám se se stejným problémem.
Mohl bych tě poprosit o update toho repository? Nenašel jsem tam to dědění CoreMappingPluginu a implementaci SymfonySerializer.
Díky!

Editoval kleinpeter (15. 10. 2022 20:51)

mcmatak
Člen | 504
+
0
-

Richard Faila napsal(a):

Nakonec jsem to vyřešil tak, jak jsem uváděl v https://forum.nette.org/…orene-entity#… tj. podědil jsem si CoreMappingPlugin a doplnil do něj podporu pro Symfony\Serialiazer, který se potom používá v metodě createEntity třídy Apitte\Core\Mapping\RequestEntityMapping kterou jsem musel taky podědit a přepsat.

mohu taky poprosit podívat jak si to vyřešil? mám stejný problém

Felix
Nette Core | 1245
+
0
-

Pouzit vlastni implementaci IEntityValidatoru by neslo?

V apitte je aktualne predpripraveny SymfonyValidator (https://github.com/…alidator.php)

api:
    plugins:
        Apitte\Core\DI\Plugin\CoreMappingPlugin:
          request:
            validator: Apitte\Core\Mapping\Validator\SymfonyValidator()

Bud pouzit ten pripraveny a nebo splnit interface IEntityValidator a naimplementovat si svoje?

rasstislav
Člen | 2
+
0
-

@Felix problém nie je vo validácii, to je až výsledok problému (dôsledok), ktorý je spôsobený inde a teda treba odstrániť príčinu a to presne tú, ako píše @RichardFaila, že pri mapovaní sa nevie vytvoriť vnorená entita, teda napríklad user, krorý má role alebo produkt, ktorý má kategóriu a pod.

Ja som si to nejako ohackoval, taktiež s využitím Symfony/Serializer, ale by default by to malo fungovať už v core balíčka.