This is my Cest class:
<?php
namespace App\Tests\functional;
use App\Entity\AssetsRecord;
use App\Entity\Provider;
use App\Entity\User;
use App\Tests\FunctionalTester;
use App\Tests\Page\Functional\AssetsRecordIndexPage;
class AssetsRecordIndexCest
{
const FIRST_PROVIDER_NAME = "Provider 1";
const SECOND_PROVIDER_NAME = "Provider 2";
const THIRD_PROVIDER_NAME = "Provider 3";
const FIRST_ASSETS_RECORD_VALUE = 1000;
const SECOND_ASSETS_RECORD_VALUE = 2000;
const THIRD_ASSETS_RECORD_VALUE = 3000;
private array $providers;
/**
* #var array <AssetsRecord>
*/
private array $assetsRecords = [];
private User $singleProviderOperator;
private User $doubleProviderOperator;
public function _before(FunctionalTester $I)
{
$this->buildProviders($I);
$I->haveProvidersInDB($this->providers);
$this->buildAssetsRecords($I);
$I->haveAssetsRecordsInDB($this->assetsRecords);
$this->buildOperators($I);
$I->haveInRepository($this->singleProviderOperator);
$I->haveInRepository($this->doubleProviderOperator);
}
// tests
public function shouldNotShowAssetRecordsForNotEnabledProviders(FunctionalTester $I, AssetsRecordIndexPage $page)
{
$I->amLoggedInAs($this->singleProviderOperator);
$page->doesntShow(...$this->getAssetsRecordsIn($this->providers[self::SECOND_PROVIDER_NAME]));
$page->doesntShow(...$this->getAssetsRecordsIn($this->providers[self::THIRD_PROVIDER_NAME]));
$I->amLoggedInAs($this->doubleProviderOperator);
$page->doesntShow(...$this->getAssetsRecordsIn($this->providers[self::FIRST_PROVIDER_NAME]));
}
public function shouldShowAllAssetsRecordsForEnabledProviders(FunctionalTester $I, AssetsRecordIndexPage $page)
{
$I->amLoggedInAs($this->singleProviderOperator);
$page->shows(...$this->getAssetsRecordsIn($this->providers[self::FIRST_PROVIDER_NAME]));
$I->amLoggedInAs($this->doubleProviderOperator);
$page->shows(...$this->getAssetsRecordsIn($this->providers[self::SECOND_PROVIDER_NAME]));
$page->shows(...$this->getAssetsRecordsIn($this->providers[self::THIRD_PROVIDER_NAME]));
}
/**
* #param Provider $provider
* #return array
*/
private function getAssetsRecordsIn(Provider $provider): array
{
return array_filter($this->assetsRecords, fn(AssetsRecord $assetsRecord) => $assetsRecord->getProvider() === $provider);
}
/**
* #param FunctionalTester $I
* #return void
*/
private function buildAssetsRecords(FunctionalTester $I): void
{
$this->assetsRecords[] = $I->buildAssetsRecord($this->providers[self::FIRST_PROVIDER_NAME], self::FIRST_ASSETS_RECORD_VALUE);
$this->assetsRecords[] = $I->buildAssetsRecord($this->providers[self::SECOND_PROVIDER_NAME], self::SECOND_ASSETS_RECORD_VALUE);
$this->assetsRecords[] = $I->buildAssetsRecord($this->providers[self::THIRD_PROVIDER_NAME], self::THIRD_ASSETS_RECORD_VALUE);
}
/**
* #param FunctionalTester $I
* #return void
*/
private function buildProviders(FunctionalTester $I): void
{
$this->providers[self::FIRST_PROVIDER_NAME] = $I->buildProvider(self::FIRST_PROVIDER_NAME);
$this->providers[self::SECOND_PROVIDER_NAME] = $I->buildProvider(self::SECOND_PROVIDER_NAME);
$this->providers[self::THIRD_PROVIDER_NAME] = $I->buildProvider(self::THIRD_PROVIDER_NAME);
}
/**
* #param FunctionalTester $I
* #return void
*/
private function buildOperators(FunctionalTester $I): void
{
$this->singleProviderOperator = $I->buildOperatorUserWithProvidersEnabled(
$this->providers[self::FIRST_PROVIDER_NAME]
);
$this->doubleProviderOperator = $I->buildOperatorUserWithProvidersEnabled(
$this->providers[self::SECOND_PROVIDER_NAME],
$this->providers[self::THIRD_PROVIDER_NAME]
);
}
}
When I run the whole class I get this error:
A new entity was found through the relationship 'App\Entity\AssetsRecord#provider' that was not configured to cascade persist operations for entity: Provider 1. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
But if I run each test separetely it works ok and the tests are succesful...
Any idea why this is happening? Thanks!
Related
I'm writing PHPUnit tests and running coverage tests. I admit its very difficult to have 100% coverage, however, I'd like to get as close as possible. In a following scenario, how to mock variables in a clause in order to test the code block?
class CalendarClientService
{
/** #var array SCOPES */
public const SCOPES = [Google_Service_Calendar::CALENDAR];
/** #var string ACCESS_TYPE */
public const ACCESS_TYPE = "offline";
/** #var string CALENDAR_ID */
public const CALENDAR_ID = "primary";
/** #var int MAX_RESULTS */
public const MAX_RESULTS = 25;
/** #var string ORDER_BY */
public const ORDER_BY = "startTime";
/** #var bool SINGLE_EVENTS */
public const SINGLE_EVENTS = true;
/** #var string|null TIME_MIN */
public const TIME_MIN = null;
/** #var bool CACHE_TIME_TO_LIVE */
public const CACHE_TIME_TO_LIVE = 604800;
/** #var string */
public string $clientSecretPath = "";
/** #var StorageAdapterFactoryInterface */
protected StorageAdapterFactoryInterface $storageAdapterFactory;
/** #var StorageInterface */
protected StorageInterface $storageInterfaceCache;
/**
* CalendarClientService constructor.
* #param string $clientSecretPath
* #param StorageAdapterFactoryInterface $storageAdapterFactory
* #param StorageInterface $storageInterfaceCache
*/
public function __construct(
string $clientSecretPath,
StorageAdapterFactoryInterface $storageAdapterFactory,
StorageInterface $storageInterfaceCache
) {
$this->clientSecretPath = $clientSecretPath;
$this->storageAdapterFactory = $storageAdapterFactory;
$this->storageInterfaceCache = $storageInterfaceCache;
}
/** #return string */
public function getClientSecretPath()
{
return $this->clientSecretPath;
}
/** #param string $secretFile */
public function setClientSecretPath(string $secretFile)
{
$this->clientSecretPath = $secretFile;
}
/**
* #param array
* #return Google_Service_Calendar_Event
*/
public function getGoogleServiceCalendarEvent($eventData)
{
return new Google_Service_Calendar_Event($eventData);
}
/**
* #param string
* #return Google_Service_Calendar_EventDateTime
*/
public function getGoogleServiceCalendarEventDateTime($dateTime)
{
$eventDateTime = new Google_Service_Calendar_EventDateTime();
$eventDateTime->setDateTime(Carbon::parse($dateTime)->toW3cString());
$eventDateTime->setTimeZone(Carbon::parse($dateTime)->timezone->getName());
return $eventDateTime;
}
/**
* #param Google_Client $client
* #return Events
*/
public function getGoogleServiceCalendarResourceEvents(Google_Client $client)
{
$service = new Google_Service_Calendar($client);
return $service->events;
}
/**
* #param int
* #return array
* #throws Exception
* #throws ExceptionInterface
*/
public function getEventData($id)
{
$client = $this->getClient();
if (!$this->authenticateClient($client)) {
return [
"error" => "authentication",
"url" => filter_var($client->createAuthUrl(), FILTER_SANITIZE_URL),
];
}
$service = $this->getGoogleServiceCalendarResourceEvents($client);
return ["event" => $service->get(self::CALENDAR_ID, $id)];
}
/**
* #return Google_Client
* #throws Exception
*/
public function getClient()
{
$client = new Google_Client();
$client->setApplicationName(Module::MODULE_NAME);
$client->setScopes(self::SCOPES);
$client->setAuthConfig($this->clientSecretPath);
$client->setAccessType(self::ACCESS_TYPE);
return $client;
}
/**
* #param Google_Client $client
* #return bool
* #throws ExceptionInterface
*/
public function authenticateClient(Google_Client $client)
{
if ($this->storageInterfaceCache->hasItem("api_access_token")) {
$accessToken = json_decode($this->storageInterfaceCache->getItem("api_access_token"), true);
if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
$this->storageInterfaceCache->removeItem("api_access_token");
} else {
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$client->setAccessToken($accessToken);
}
}
if ($client->isAccessTokenExpired()) {
$tokenValid = false;
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
$accessToken = $client->getAccessToken();
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$tokenValid = true;
} else {
$helper = new Helper();
if(!$helper->verifyAuthCode($_GET["code"])){
return $tokenValid;
}
$authCode = $_GET["code"];
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
$this->storageInterfaceCache->removeItem("api_access_token");
} else {
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$client->setAccessToken($accessToken);
$tokenValid = true;
}
}
} else {
$tokenValid = true;
}
return isset($tokenValid) ? $tokenValid : false;
}
I want to test this 6th line from top in authenticateClient method and want to mock this clause $accessToken["error"] == "invalid_grant" || empty($accessToken).
Now how to go about it?
Edit: Here's a test that I've written. Now whatever value I'm mocking in the $this->storageInterfaceCacheMock->method("getItem"), it always returns empty $accessToken. I've also attached the image for better understanding of what's happening and what I want.
public function testGetEventDataReturnsArrayOnSuccessfulAuthenticateClientThroughCache()
{
$this->storageInterfaceCacheMock->method("hasItem")->willReturn(true);
$this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"ya29.a0ARrdaM99pJTf1XzmD1ngxAH3XJud8lvHb0aTaOOABYdfdhsdfgsdfgVD9OoH4heiKoskDF7DMkHj1_aPuWIO5TE14KHJidFf66xwn_pTCkkSow6Kg4lRHwGrNQBQGI8sPlgnFO5U5hJvYdqgxDMHEqw1TER2w","expires_in":3599,"scope":"https:\/\/www.googleapis.com\/auth\/calendar","token_type":"Bearer","created":1637312218,"refresh_token":"1\/\/03psr5omKiljUCgYFDHDGJHGSHSNwF-L9Iraor5zcfe-h3BeCHSFGSDFGDGJHjy4UnEtKj974LXthS5bWexQcjviVGfJsdfGHSHgIrDn6Yk"}'));
$this->assertIsArray($this->calendarClientService->getEventData(1));
}
Another test which isn't performing as per required is mentioned below. (also visible in the screenshot)
public function testAccessTokenIsExpiredAndGotRefreshToken()
{
$this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
$this->googleClientMock->method("getRefreshToken")->willReturn(true);
$this->googleClientMock->method("fetchAccessTokenWithRefreshToken")->willReturnSelf();
$this->googleClientMock->method("getAccessToken")->willReturnSelf();
$this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
}
I believe you want json_decode you don't need double encode here:
$this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"...."}'));
just
$this->storageInterfaceCacheMock->method("getItem")->willReturn('{"access_token":"...."}');
Here's how I resolved the issue and got the test to be successful.
I declared a dummy ACCESS_TOKEN as below and then used in the test method.
class CalendarControllerTest extends AbstractApplicationTestCase
{
/** #var string CLIENT_SECRET */
public const CLIENT_SECRET = __DIR__ . "/../_fixtures/config/client_secret.json";
/** #var string CLIENT_SECRET */
public const ACCESS_TOKEN = [
"access_token" => "test-data",
"expires_in" => 3592,
"scope" => "https://www.googleapis.com/auth/calendar",
"token_type" => "Bearer",
"created" => 1640858809,
];
...
...
...
}
public function setUp(): void
{
parent::setUp();
$this->googleClientMock = $this->getMockBuilder(Google_Client::class)
->disableOriginalConstructor()
->onlyMethods(
[
"isAccessTokenExpired",
"setAuthConfig",
"getRefreshToken",
"fetchAccessTokenWithRefreshToken",
"fetchAccessTokenWithAuthCode",
"getAccessToken",
"setAccessToken",
]
)
->getMock();
$this->googleServiceCalendarResourceEventsMock = $this->getMockBuilder(Events::class)
->disableOriginalConstructor()
->onlyMethods(["get"])
->getMock();
$this->googleServiceCalendarEventMock = $this->getMockBuilder(Event::class)
->disableOriginalConstructor()
->getMock();
$this->storageInterfaceCacheMock = $this->getMockForAbstractClass(StorageInterface::class);
$this->container->setAllowOverride(true);
$this->container->setService(Google_Client::class, $this->googleClientMock);
$this->container->setService(Events::class, $this->googleServiceCalendarResourceEventsMock);
$this->container->setService(Event::class, $this->googleServiceCalendarEventMock);
$this->container->setService(StorageInterface::class, $this->storageInterfaceCacheMock);
$this->container->setAllowOverride(true);
$this->googleClientMock->method("setAuthConfig")->willReturn(true);
$this->calendarClientService = $this->container->get("ServiceManager")->get(CalendarClientService::class);
$this->calendarClientService->setClientSecretPath(CalendarControllerTest::CLIENT_SECRET);
}
/** #throws ExceptionInterface */
public function testAccessTokenIsExpiredAndFailureToRefreshTokenWillGenerateNewAccessToken()
{
$this->calendarClientService->setAuthCode(CalendarControllerTest::DEFAULT_TESTING_VALUE);
$this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
$this->googleClientMock->method("getRefreshToken")->willReturn(false);
$this->googleClientMock->method("fetchAccessTokenWithAuthCode")->willReturn(
CalendarControllerTest::ACCESS_TOKEN
);
$this->storageInterfaceCacheMock->method("setItem")->willReturn(true);
$this->googleClientMock->method("setAccessToken")->willReturnSelf();
$this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
}
I'm using jms/serializer-bundle 2.4.3 on a symfony 4.2 and a I noticed an annoying problem in my application :
when I post an entity, the DoctrineObjectConstructor uses id in content to retrieve another entity and thus patch it while it is excluded by my security groups
see rather entity
class Entity
{
/**
* #var int
*
* #ORM\Column(name="id", type="int")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #Serializer\Groups({"GetEntity"})
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
* #Serializer\Groups({"GetEntity", "PostEntity"})
*/
private $name;
}
controller
/**
* #Route("/entity", name="post_entity", methods={"POST"})
*/
public function postEntity(Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): JsonResponse
{
$deserializationContext = DeserializationContext::create();
$deserializationContext->setGroups(['PostEntity']);
$entity = $serializer->deserialize($request->getContent(), Entity::class, 'json', $deserializationContext);
$entityManager->persist($entity);
$entityManager->flush();
return $this->json($entity, Response::HTTP_OK, [], ['groups' => ['GetEntity']]);
}
I have some JMS configurations changes in services
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: true
jms_serializer.unserialize_object_constructor:
class: App\Serializer\ObjectConstructor
If anyone can explain to me how to ignore the id in this case I'm open to any suggestions.
Regards and thanks for any help
To resolve, just add override in your services.yaml
jms_serializer.doctrine_object_constructor:
class: App\Serializer\DoctrineObjectConstructor
arguments:
- '#doctrine'
- '#jms_serializer.unserialize_object_constructor'
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
and add a local DoctrineObjectConstructor updated to ignore entities without current deserialization group on id property
class DoctrineObjectConstructor implements ObjectConstructorInterface
{
const ON_MISSING_NULL = 'null';
const ON_MISSING_EXCEPTION = 'exception';
const ON_MISSING_FALLBACK = 'fallback';
private $fallbackStrategy;
private $managerRegistry;
private $fallbackConstructor;
/**
* Constructor.
*
* #param ManagerRegistry $managerRegistry Manager registry
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
* #param string $fallbackStrategy
*/
public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, $fallbackStrategy = self::ON_MISSING_NULL)
{
$this->managerRegistry = $managerRegistry;
$this->fallbackConstructor = $fallbackConstructor;
$this->fallbackStrategy = $fallbackStrategy;
}
/**
* {#inheritdoc}
*/
public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context)
{
// Locate possible ObjectManager
$objectManager = $this->managerRegistry->getManagerForClass($metadata->name);
if (!$objectManager) {
// No ObjectManager found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Locate possible ClassMetadata
$classMetadataFactory = $objectManager->getMetadataFactory();
if ($classMetadataFactory->isTransient($metadata->name)) {
// No ClassMetadata found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Managed entity, check for proxy load
if (!\is_array($data)) {
// Single identifier, load proxy
return $objectManager->getReference($metadata->name, $data);
}
// Fallback to default constructor if missing identifier(s)
$classMetadata = $objectManager->getClassMetadata($metadata->name);
$identifierList = [];
foreach ($classMetadata->getIdentifierFieldNames() as $name) {
$propertyGroups = [];
if ($visitor instanceof AbstractVisitor) {
/** #var PropertyNamingStrategyInterface $namingStrategy */
$namingStrategy = $visitor->getNamingStrategy();
$dataName = $namingStrategy->translateName($metadata->propertyMetadata[$name]);
$propertyGroups = $metadata->propertyMetadata[$name]->groups;
} else {
$dataName = $name;
}
if (!array_key_exists($dataName, $data) || true === empty(array_intersect($context->getAttribute('groups'), $propertyGroups))) {
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
$identifierList[$name] = $data[$dataName];
}
// Entity update, load it from database
$object = $objectManager->find($metadata->name, $identifierList);
if (null === $object) {
switch ($this->fallbackStrategy) {
case self::ON_MISSING_NULL:
return null;
case self::ON_MISSING_EXCEPTION:
throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));
case self::ON_MISSING_FALLBACK:
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
default:
throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
}
}
$objectManager->initializeObject($object);
return $object;
}
}
I am attempting to create a module which defines it's own custom Type and associated Field plugins.
When installed, GraphQLi reports the following error in the console:
Uncaught Error: CustomTypeInterface fields must be an object with field names as keys or a function which returns such an object.
Drupal 8.61. I have tried on both GraphQL 3.0-RC2 and 3.x-Dev. Any help would be much appreciated. Thanks.
My code is as follows:
/graphql_custom.info.yml
name: GraphQL Custom Type Example
type: module
description: ''
package: GraphQL
core: 8.x
dependencies:
- graphql_core
/src/CustomObject.php
namespace Drupal\graphql_custom;
class CustomObject {
protected $data;
function __construct(String $data) {
$this->data = $data;
}
function getData() {
return $this->data;
}
}
/src/Plugin/GraphQL/Fields/CustomField.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Fields;
use Drupal\graphql_custom\CustomObject;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use GraphQL\Type\Definition\ResolveInfo;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Created Custom Object with argument as data.
*
* #GraphQLField(
* id = "custom_field",
* secure = true,
* name = "customfield",
* type = "CustomType",
* nullable = true,
* arguments = {
* "argument" = "String!"
* }
* )
*/
class CustomField extends FieldPluginBase implements ContainerFactoryPluginInterface {
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {#inheritdoc}
*/
protected function isLanguageAwareField() {
return FALSE;
}
/**
* {#inheritdoc}
*/
public function resolve($value, array $args, ResolveContext $context, ResolveInfo $info) {
return parent::resolve($value, $args, $context, $info);
}
/**
* {#inheritdoc}
*/
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
$arg = $args['argument'];
$object = new CustomObject($arg);
yield $object;
}
}
/src/Plugin/GraphQL/Fields/CustomFieldData.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Fields;
use Drupal\graphql_custom\CustomObject;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Custom Type Data Field
*
* #GraphQLField(
* id = "custom_field_data",
* secure = true,
* name = "data",
* type = "String",
* parents = {"CustomType"}
* )
*/
class CustomFieldData extends FieldPluginBase {
/**
* {#inheritdoc}
*/
protected function resolveValues($value, array $args, $context, $info) {
if ($value instanceOf CustomObject) {
yield (string) $value->getData();
} else {
yield (string) "Empty";
}
}
}
/src/Plugin/GraphQL/Interfaces/CustomTypeInterface.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Interfaces;
use Drupal\graphql_custom\CustomObject;
use Drupal\graphql\Annotation\GraphQLInterface;
use Drupal\graphql\Plugin\GraphQL\Interfaces\InterfacePluginBase;
/**
* Interface for Custom Type.
*
* For simplicity reasons, this example does not utilize dependency injection.
*
* #GraphQLInterface(
* id = "custom_type_interface",
* name = "CustomTypeInterface"
* )
*/
class CustomTypeInterface extends InterfacePluginBase {
/**
* {#inheritdoc}
*/
public function resolveType($object) {
if ($object instanceof CustomObject) {
$schemaManager = \Drupal::service('graphql_core.schema_manager');
return $schemaManager->findByName('CustomType', [
GRAPHQL_CORE_TYPE_PLUGIN,
]);
}
}
}
/src/Plugin/GraphQL/Types/CustomType.php
<?php
namespace Drupal\graphql_custom\Plugin\GraphQL\Types;
use Drupal\graphql_custom\CustomObject;
use Drupal\graphql\Plugin\GraphQL\Types\TypePluginBase;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use GraphQL\Type\Definition\ResolveInfo;
/**
* GraphQL Custom Type.
*
* #GraphQLType(
* id = "custom_type",
* name = "CustomType",
* interfaces = {"CustomTypeInterface"}
* )
*/
class CustomType extends TypePluginBase {
/**
* {#inheritdoc}
*/
public function applies($object, ResolveContext $context, ResolveInfo $info) {
return $object instanceof CustomObject;
}
}
With your fields you should use interface reference in parents instead of type:
parents = {"CustomTypeInterface"}
Another way is to remove interface and use direct type reference as mentioned in your example.
We have a legacy app which is not based on symfony. Doctrine is in use and now we would like to add validation to the models. Seems that the Annotations never get autoloaded, even when "use" statements are in use.
[Semantical Error] The annotation "#Symfony\Component\Validator\Constraints\NotBlank" in property Test\Stackoverflow\User::$Username does not exist, or could not be auto-loaded.
Wrote a small demo application to showcase the problem and how we create the entity manager and validation instance.
composer.json:
{
"require": {
"symfony/validator" : "~3.1"
, "doctrine/orm" : "~2.6.1"
}
}
index.php
require_once ('vendor/autoload.php');
// Load Entities, would normally be done over composer since they reside in a package
require_once('test/User.php');
require_once('MyAnnotationTestApp.php');
// create test app
$app = new MyAnnotationsTestApp();
$app->initEntityManager('localhost', 'annotation_test', 'root', 'mysql', 3306);
if(key_exists('test', $_GET)){
// Create entity and validate it
$entity = new \Test\Stackoverflow\User();
$entity->setUsername('StackoverflowUser');
if($app->testAnnotationWithoutLoading($entity)){
print "Seems the validation was working without preloading the asserts\n<br>";
}
if($app->testAnnotationWithLoading($entity)){
print "Seems the validation was working because we loaded the required class ourself.\n<br>";
}
print "\n<br><br>The question is why the required annotation classes never get autoloaded?";
}else{
// Load the validator class otherwise the annotation throws an exception
$notBlankValidator = new \Symfony\Component\Validator\Constraints\NotBlank();
print "We have cerated the tables but also had to load the validator class ourself.\n<br>\n<br>";
// create tables and
$app->updateDatabaseSchema();
print sprintf('Now lets run the test', $_SERVER['REQUEST_URI']);
}
Doctrine user Entity
<?php
namespace Test\Stackoverflow;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity()
* #ORM\Table(name="users")
*
*/
class User{
/**
* #ORM\Id
* #ORM\Column(name="Id",type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $Id;
public function getId(){
return $this->Id;
}
/**
* #ORM\Column(type="text", length=80, nullable=false)
* #Assert\NotBlank()
*/
protected $Username;
/**
* #return string
*/
public function getUsername()
{
return $this->Username;
}
/**
* #param string $Username
*/
public function setUsername($Username)
{
$this->Username = $Username;
}
}
Demo App with doctrine/validator initialisation:
<?php
final class MyAnnotationsTestApp {
/**
* #var \Doctrine\ORM\EntityManager
*/
private $entityManager;
/**
* #param string $host
* #param string $database
* #param string $username
* #param string $password
* #param integer $port
* #param array $options
* #return \Doctrine\ORM\EntityManager
*/
public function initEntityManager($host, $database, $username, $password, $port, array $options=null){
if($this->entityManager){
return $this->entityManager;
}
$connectionString = sprintf('mysql://%3$s:%4$s#%1$s/%2$s', $host, $database, $username, $password, $port);
$isDevMode = true;
$dbParams = array(
'url' => $connectionString
, 'driver' => 'pdo_mysql'
, 'driverOptions' => array(
1002 => "SET NAMES utf8mb4"
)
);
$cacheDriver = null;
$config = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(array(), $isDevMode, '.cache/', $cacheDriver, false);
if($cacheDriver){
$config->setMetadataCacheImpl($cacheDriver);
$config->setQueryCacheImpl($cacheDriver);
$config->setResultCacheImpl($cacheDriver);
}
$this->entityManager = \Doctrine\ORM\EntityManager::create($dbParams, $config);
return $this->entityManager;
}
/**
* #return \Doctrine\ORM\EntityManager
*/
public function getEntityManager(){
return $this->entityManager;
}
public function updateDatabaseSchema(){
$metaData = array();
$usedEntities = array(
'Test\Stackoverflow\User'
);
foreach($usedEntities as $entity){
$metaData[] = $this->entityManager->getClassMetadata($entity);
}
$tool = new \Doctrine\ORM\Tools\SchemaTool($this->entityManager);
$tool->updateSchema($metaData);
$this->generateProxies($metaData);
}
/**
* Generate all the proxy classes for orm in the correct directory.
* Proxy dir can be configured over application configuration
*
*
* #throws \Exception
*/
final public function generateProxies($metaData)
{
$em = $this->getEntityManager();
$destPath = $em->getConfiguration()->getProxyDir();
if (!is_dir($destPath)) {
mkdir($destPath, 0777, true);
}
$destPath = realpath($destPath);
if (!file_exists($destPath)) {
throw new \Exception("Proxy destination directory could not be created " . $em->getConfiguration()->getProxyDir());
}
if (!is_writable($destPath)) {
throw new \Exception(
sprintf("Proxies destination directory '<info>%s</info>' does not have write permissions.", $destPath)
);
}
if (count($metaData)) {
// Generating Proxies
$em->getProxyFactory()->generateProxyClasses($metaData, $destPath);
}
}
/**
* #var \Symfony\Component\Validator\Validator\ValidatorInterface
*/
protected $validator;
/**
* #return \Symfony\Component\Validator\Validator\ValidatorInterface
*/
final protected function getValidator(){
if($this->validator){
return $this->validator;
}
$this->validator = \Symfony\Component\Validator\Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator();
return $this->validator;
}
/**
* #param \Test\Stackoverflow\User $entity
* #return bool
*/
final public function testAnnotationWithoutLoading(\Test\Stackoverflow\User $entity){
try {
print "test to validate the entity without preloading the Assert classes\n<br>";
$this->getValidator()->validate($entity);
return true;
} catch(\Exception $e){
print "<strong>Does not work since the Asserts classes never get loaded: </strong> Exception-message: ".$e->getMessage()."\n<br>";
return false;
}
}
/**
* #param \Test\Stackoverflow\User $entity
* #return bool
*/
final public function testAnnotationWithLoading(\Test\Stackoverflow\User $entity){
// Here we force the autoloader to require the class
$notBlankValidator = new \Symfony\Component\Validator\Constraints\NotBlank();
try {
print "Loaded the validator manually, will test of it fails now\n<br>";
$this->getValidator()->validate($entity);
return true;
} catch(\Exception $e){
print "<strong>Was not working: </strong> Exception-message: ".$e->getMessage()."\n<br>";
print sprintf("<strong>Even when we autoload the class it is not working. Type of assert: %s</strong>\n<br>", get_class($notBlankValidator));
return false;
}
}
}
If you are using the Symfony Standard Edition, you must update your
autoload.php file by adding the following code [1]
How are these annotations loaded? From looking at the code you could
guess that the ORM Mapping, Assert Validation and the fully qualified
annotation can just be loaded using the defined PHP autoloaders. This
is not the case however: For error handling reasons every check for
class existence inside the AnnotationReader sets the second parameter
$autoload of class_exists($name, $autoload) to false. To work
flawlessly the AnnotationReader requires silent autoloaders which many
autoloaders are not. Silent autoloading is NOT part of the PSR-0
specification for autoloading. [2]
// at the top of the file
use Doctrine\Common\Annotations\AnnotationRegistry;
// at the end of the file
AnnotationRegistry::registerLoader(function($class) use ($loader) {
$loader->loadClass($class);
return class_exists($class, false);
});
[1] https://symfony.com/blog/symfony2-2-0-rc4-released
[2] https://www.doctrine-project.org/projects/doctrine-annotations/en/1.6/annotations.html
I want to allow user creating oEmbed content to my website.
There is a lot of information how to implement it but none about creating a enpoint.
Quick question: is there some ready php code samples for creating (returning) oEmbed response for third party websites?
If no, how to create it? Ex youtube link looks like this:
http://www.youtube.com/oembed?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiwGFalTRHDA
and the response is :
{
"html": "<iframe width=\"459\" height=\"344\" src=\"https://www.youtube.com/embed/iwGFalTRHDA?feature=oembed\" frameborder=\"0\" allowfullscreen></iframe>",
"thumbnail_height": 360,
"thumbnail_width": 480,
"provider_name": "YouTube",
"author_url": "https://www.youtube.com/user/KamoKatt",
"thumbnail_url": "https://i.ytimg.com/vi/iwGFalTRHDA/hqdefault.jpg",
"author_name": "KamoKatt",
"provider_url": "https://www.youtube.com/",
"type": "video",
"version": "1.0",
"width": 459,
"title": "Trololo",
"height": 344
}
My question is: how they know which video it is? They are using regexp to parse videoID?
What is 'best practice' for this kind of request? Should it be created as Controller, Service, Provider or how?
Finally I have created it like this:
I have created a service:
namespace AppBundle\Service;
use AppBundle\Entity\PlaceMarker;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception as Ex;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class EmbedService
{
CONST THUMBNAIL_WIDTH = 400;
CONST THUMBNAIL_HEIGHT = 300;
CONST HTML_WIDTH = 459;
CONST HTML_HEIGHT = 344;
/**
* #var string
*/
protected $baseUrl;
/**
* #var array
*/
private $params = [];
/**
* #var EntityManager
*/
private $entityManager;
/** #var \Twig_Environment */
private $twig;
public function __construct($baseUrl, EntityManager $entityManager, \Twig_Environment $twig)
{
$this->baseUrl = strtolower($baseUrl);
$this->entityManager = $entityManager;
$this->twig = $twig;
}
/**
*
* #param Request $request
* #param null $format
* #throws \Exception
*/
public function handleRequest(Request $request, $format = null)
{
$this->params = [];
// $baseUrl = $request->getSchemeAndHttpHost();
/** handle url parameter */
$this->params['url'] = $request->query->get('url');
if (!$this->params['url']) {
throw new Ex\BadRequestHttpException('A URL parameter must be provided');
}
/** check if url matches site url */
$this->params['path'] = substr($request->query->get('url'), strlen($this->baseUrl));
if (substr(strtolower($request->query->get('url')), 0, strlen($this->baseUrl)) !== strtolower($this->baseUrl)) {
throw new Ex\BadRequestHttpException('This is not a valid URL parameter');
}
/** handle format parameters */
$this->params['format'] = $format;
if (null === $format) {
$this->params['format'] = $request->query->get('format', 'json');
}
if (!in_array($this->params['format'], ['json', 'xml'])) {
throw new \Exception('Disallowed format parameter \'' . $this->params['format'] . '\'');
}
$maxWidth = $request->query->get('maxwidth', null);
if (is_numeric($maxWidth) || is_null($maxWidth)) {
$this->params['maxWidth'] = $maxWidth;
}
$maxHeight = $request->query->get('maxheight', null);
if (is_numeric($maxHeight) || is_null($maxHeight)) {
$this->params['maxHeight'] = $maxHeight;
}
$this->params['attr'] = $this->matchRoute($this->params['path']);
}
public function matchRoute($path)
{
$route1 = new Route('/place-marker/{id}/show', ['controller' => 'PlaceMarkerController']);
$routes = new RouteCollection();
$routes->add('place-marker', $route1);
$context = new RequestContext('/');
$matcher = new UrlMatcher($routes, $context);
try {
return $matcher->match($path);
} catch (\Exception $ex) {
throw new Ex\NotFoundHttpException('No implemented!');
}
}
/**
* #return array
*/
public function create()
{
if (!$this->params) {
throw new Exception('Unable to create oEmbed. Did you use handleRequest() first?');
}
$oEmbed = [
'version' => '1.0',
'provider_url' => $this->baseUrl,
'provider_name' => 'Pixdor.com'
];
switch ($this->params['attr']['_route']) {
case 'place-marker':
/**
* #var PlaceMarker $pMarker
*/
$pMarker = $this->entityManager->getRepository(PlaceMarker::class)->find($this->params['attr']['id']);
$oEmbed['title'] = $pMarker->getName();
$oEmbed['url'] = $pMarker->getUrl();
$oEmbed['description'] = $pMarker->getDescription();
$oEmbed['caption'] = $pMarker->getCaption();
$oEmbed['html'] = $this->twig->render(':place_marker:embed.html.twig');
$oEmbed['width'] = self::HTML_WIDTH;
$oEmbed['height'] = self::HTML_HEIGHT;
$oEmbed['rich'] = 'photo';
break;
}
$oEmbed['thumbnail_width'] = isset($this->params['maxWidth']) ? $this->params['maxWidth'] : self::THUMBNAIL_WIDTH;
$oEmbed['thumbnail_height'] = isset($this->params['thumbnail_height']) ? $this->params['thumbnail_height'] : self::THUMBNAIL_HEIGHT;
$oEmbed['thumbnail_url'] = sprintf('http://placehold.it/%dx%d', $oEmbed['thumbnail_width'], $oEmbed['thumbnail_height']);
return $oEmbed;
}
/**
* Returns format type (json|xml)
* #return string
*/
public function getFormat()
{
if (!$this->params) {
throw new Exception('Unknown format type. Did you use handleRequest() first?');
}
return $this->params['format'];
}
}
services.yml
app.embed:
class: AppBundle\Service\EmbedService
arguments: ["%base_url%", "#doctrine.orm.entity_manager", "#twig"]
parameters.yml
base_url: "http://project.local"
next in the controller:
/**
* #Route("/oembed.{_format}",
* name="embed",
* defaults={"_format": null}
* )
* #param Request $request
* #param $_format
* #return \Symfony\Component\HttpFoundation\Response
* #throws \Exception
*/
public function generateAction(Request $request, $_format)
{
$oEmbed = $this->get('app.embed');
$oEmbed->handleRequest($request, $_format);
$view = View::create()
->setFormat($oEmbed->getFormat())
->setStatusCode(200)
->setData($oEmbed->create());
return $this->handleView($view);
}
you call it like this:
http://project.local/oembed?url=http://project.local/place-marker/4/show