a PHPpunit test fail randomly - php

I have a Quiz class.
This class load 10 questions from a database depending on the level and the type of the quiz Object:
level 0 load the ten first, level 1 load the next ten and so on.
So in my test i create in a test database 30 questions.
Then i create quiz object with different level and i check that the first question in the quiz steps array match what i expect.
This test "quiz_contain_steps_depending_on_type_and_level()" failed randomly at least once every 5 launches.
This is the QuizTest class
<?php
namespace App\Tests\Quiz;
use App\Quiz\Question;
use App\Quiz\Quiz;
use App\Quiz\QuizQuestionRepositoryManager;
use App\Quiz\QuizStep;
use App\Quiz\QuizType;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectRepository;
use Faker\Factory;
use Faker\Generator;
use ReflectionException;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Config\Definition\Exception\Exception;
class QuizTest extends KernelTestCase
{
use QuestionLoremTrait;
use PrivatePropertyValueTestTrait;
private Generator $faker;
private ?EntityManagerInterface $em;
private ObjectRepository $questionRepo;
private QuizQuestionRepositoryManager $quizQuestionManager;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->faker = Factory::create();
$this->em = $kernel->getContainer()->get('doctrine')->getManager();
$this->em->getConnection()->beginTransaction();
$this->questionRepo = $kernel->getContainer()->get('doctrine')->getRepository(Question::class);
$this->quizQuestionManager = new QuizQuestionRepositoryManager($this->questionRepo);
}
protected function tearDown(): void
{
parent::tearDown();
$this->em->getConnection()->rollBack();
$this->em->close();
$this->em = null;
}
/**
* #test
* #dataProvider provideQuizDataAndFirstQuestionExpectedIndex
* #param array $quizData
* #param int $firstQuestionExpectedIndex
* #throws ReflectionException
* #throws \Exception
*/
public function quiz_contain_steps_depending_on_type_and_level(array $quizData, int $firstQuestionExpectedIndex)
{
//We have questions in db
$questions = [];
for ($q = 1; $q <= 30; $q++) {
$question = $this->persistLoremQuestion($this->faker, $this->em);
$questions[] = $question;
}
$this->em->flush();
//When we create Quiz instance $quiz
$quiz = new Quiz($this->quizQuestionManager,quizData: $quizData);
//When we look at this $quiz steps property
$quizSteps = $quiz->getSteps();
/** #var QuizStep $firstStep */
$firstStep = $quizSteps[0];
//We expect
$this->assertNotEmpty($quizSteps);
$this->assertCount(10, $quizSteps);
//We expect if quiz is type normal and level variable questions depends of level:
$this->assertEquals($firstStep->getQuestion(), $questions[$firstQuestionExpectedIndex]);
}
public function provideQuizDataAndFirstQuestionExpectedIndex(): array
{
return [
[[], 0],
[['type' => QuizType::NORMAL, 'level' => '1'], 10],
[['type' => QuizType::NORMAL, 'level' => '2'], 20]
];
}
}
This is the Trait who generate fake question
<?php
namespace App\Tests\Quiz;
use App\Quiz\Question;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Faker\Generator;
Trait QuestionLoremTrait{
/**
* This function persist a aleatory generated question, you must flush after
* #param Generator $faker
* #param EntityManagerInterface $em
* #return Question
* #throws Exception
*/
public function persistLoremQuestion(Generator $faker, EntityManagerInterface $em): Question
{
$nbrOfProps = random_int(2,4);
$answerPosition = random_int(0, $nbrOfProps - 1);
$props = [];
for ($i = 0; $i < $nbrOfProps; $i++){
$props[$i] = $faker->sentence ;
}
$question = new Question();
$question
->setSharedId(random_int(1, 2147483647))
->setInfo($faker->paragraph(3))
->setStatement($faker->sentence ."?")
->setProps($props)
->setAnswerPosition($answerPosition)
;
$em->persist($question);
return $question;
}
}
This is my Quiz class:
<?php
namespace App\Quiz;
use Symfony\Component\Config\Definition\Exception\Exception;
class Quiz
{
/**
* Quiz constructor.
* #param QuizQuestionManagerInterface $quizQuestionManager
* #param array $quizData
* This array of key->value represent quiz properties.
* Valid keys are 'step','level','type'.
* You must use QuizType constant as type value
* #param string $type
* #param int $level
* #param int $currentStep
* #param array $steps
*/
public function __construct(
private QuizQuestionManagerInterface $quizQuestionManager,
private string $type = QuizType::FAST,
private int $level = 0,
private array $quizData = [],
private int $currentStep = 0,
private array $steps = [])
{
if ($quizData != []) {
$this->hydrate($quizData);
}
$this->setSteps();
}
private function hydrate(array $quizData)
{
foreach ($quizData as $key => $value) {
$method = 'set' . ucfirst($key);
// If the matching setter exists
if (method_exists($this, $method) && $method != 'setQuestions') {
// One calls the setter.
$this->$method($value);
}
}
}
public function getCurrentStep(): int
{
return $this->currentStep;
}
public function getLevel(): int
{
return $this->level;
}
public function getType(): string
{
return $this->type;
}
public function getSteps(): array
{
return $this->steps;
}
private function setCurrentStep($value): void
{
$this->currentStep = $value;
}
private function setLevel(int $level): void
{
$this->level = $level;
}
private function setType($type): void
{
if (!QuizType::exist($type)) {
throw new Exception("This quiz type didn't exist, you must use QuizType constante to define type", 400);
}
$this->type = $type;
}
private function setSteps()
{
$this->steps = [];
$questions = $this->quizQuestionManager->getQuestions($this->type, $this->level);
foreach ($questions as $question) {
$this->steps[] = new QuizStep(question: $question);
}
}
}
This is the Question class:
<?php
namespace App\Quiz;
use App\Repository\QuestionRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass=QuestionRepository::class)
*/
class Question
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\Column(type="integer")
*/
private ?int $sharedId;
/**
* #ORM\Column(type="string", length=1000, nullable=true)
* #Assert\Length(max=1000)
*/
private ?string $info;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private ?string $statement;
/**
* #ORM\Column(type="array")
*/
private array $props = [];
/**
* #ORM\Column(type="integer")
*/
private ?int $answerPosition;
public function getId(): ?int
{
return $this->id;
}
public function getSharedId(): ?int
{
return $this->sharedId;
}
public function setSharedId(int $sharedId): self
{
$this->sharedId = $sharedId;
return $this;
}
public function getInfo(): ?string
{
return $this->info;
}
public function setInfo(?string $info): self
{
$this->info = $info;
return $this;
}
public function getStatement(): ?string
{
return $this->statement;
}
public function setStatement(?string $statement): self
{
$this->statement = $statement;
return $this;
}
public function getProps(): ?array
{
return $this->props;
}
public function setProps(array $props): self
{
$this->props = $props;
return $this;
}
public function getAnswerPosition(): ?int
{
return $this->answerPosition;
}
public function setAnswerPosition(int $answerPosition): self
{
$this->answerPosition = $answerPosition;
return $this;
}
}
If anyone understands this behavior. I thank him in advance for helping me sleep better :-)

Thanks to #AlessandroChitolina comments.
The set of questions created in my test was not always recorded in the same order by my in my database.
So instead of testing the expected question from my starting $questions array, i retrieve the questions from the database in a new $dbQuestions array. That solve my problème.
This is the new test:
/**
* #test
* #dataProvider provideQuizDataAndFirstQuestionExpectedIndex
* #param array $quizData
* #param int $firstQuestionExpectedIndex
* #throws \Exception
*/
public function quiz_contain_steps_depending_on_type_and_level(array $quizData, int $firstQuestionExpectedIndex)
{
//We have questions in db
$questions = [];
for ($q = 1; $q <= 30; $q++) {
$question = $this->persistLoremQuestion($this->faker, $this->em);
$questions[] = $question;
}
$this->em->flush();
$dbQuestions = $this->questionRepo->findAll();
//When we create Quiz instance $quiz
$quiz = new Quiz($this->quizQuestionManager,quizData: $quizData);
//When we look at this $quiz steps property
$quizSteps = $quiz->getSteps();
/** #var QuizStep $firstStep */
$firstStep = $quizSteps[0];
//We expect
$this->assertNotEmpty($quizSteps);
$this->assertCount(10, $quizSteps);
//We expect if quiz is type normal and level variable questions depends of level:
$this->assertEquals($firstStep->getQuestion(), $dbQuestions[$firstQuestionExpectedIndex]);
}

Related

With doctrine ODM, can I embed many subdocuments in a main document?

I try to save this JSON:
{
"vendorId": "vendor-fc162cdffd73",
"company": {
"companyId": "bcos1.company.1806cf97-a756-4fbf-9081-fc162cdffd73",
"companyVersion": 1,
"companyName": "Delivery Inc.",
"address": {
"streetAddress": "300 Boren Ave",
"city": "Seattle",
"region": "US-WA",
"country": "US",
"postalCode": "98109",
"storeName": "Seattle Store",
"coordinates": {
"latitude": "45.992820",
"longitude": "45.992820"
}
},
"emailAddress": "johndoe#amazon.com",
"phoneNumber": "1234567890",
"websiteUrl": "delivery.com",
"creationDate": "2022-03-06T21:00:52.222Z"
},
"creationDate": "2022-04-06T21:00:52.222Z"
}
Company is a subdocument this has address and address has coordinates subdocument.
When I try to save with Hydratation, see example:
https://www.doctrine-project.org/projects/doctrine-laminas-hydrator/en/3.0/basic-usage.html#example-4-embedded-entities
I got this error:
1) AppTest\Services\AccountsServiceTest::testNewAccount with data set #0 (array('{"companyId":"bcos1.com...222Z"}', '', ''))
array_flip(): Can only flip STRING and INTEGER values!
vendor/doctrine/doctrine-laminas-hydrator/src/DoctrineObject.php:488
vendor/doctrine/doctrine-laminas-hydrator/src/DoctrineObject.php:355
vendor/doctrine/doctrine-laminas-hydrator/src/DoctrineObject.php:165
src/App/Document/Repository/AccountRepository.php:67
In DoctrineObject line 488
protected function toOne(string $target, $value): ?object
{
$metadata = $this->objectManager->getClassMetadata($target);
if (is_array($value) && array_keys($value) !== $metadata->getIdentifier()) {
// $value is most likely an array of fieldset data
$identifiers = array_intersect_key(
$value,
array_flip($metadata->getIdentifier())
);
$object = $this->find($identifiers, $target) ?: new $target();
return $this->hydrate($value, $object);
}
return $this->find($value, $target);
}
My code:
$vendorAccountId = uniqid('vendor-account-id-');
$account = new Account();
$hydrator->hydrate($data, $account);
My main Entity:
<?php
namespace App\Document\Entity;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document(db="awesome-company", collection="Account", repositoryClass="App\Document\Repository\AccountRepository")
*/
class Account
{
/** #MongoDB\Id(name="_id") */
private string $id;
/** #MongoDB\Field(type="string", name="vendorAccountId") */
private string $vendorAccountId;
/**
* #return string
*/
public function getVendorAccountId(): string
{
return $this->vendorAccountId;
}
/**
* #param string $vendorAccountId
*/
public function setVendorAccountId(string $vendorAccountId): void
{
$this->vendorAccountId = $vendorAccountId;
}
/**
* #MongoDB\EmbedOne(targetDocument=Company::class)
*/
private Company $company;
/**
* #MongoDB\Field(type="string",name="realm")
**/
private $realm;
/**
* #MongoDB\Field(type="string",name="domain")
**/
private $domain;
/**
* #MongoDB\Field(type="date",name="created_at")
**/
private \DateTime $createdAt;
public function __construct()
{
$this->company = new Company();
$this->createdAt = new \DateTime();
}
/**
* #return mixed
*/
public function getCompany()
{
return $this->company;
}
/**
* #param mixed $company
*/
public function setCompany($company): void
{
$this->company = $company;
}
/**
* #return mixed
*/
public function getRealm()
{
return $this->realm;
}
/**
* #param mixed $realm
*/
public function setRealm($realm): void
{
$this->realm = $realm;
}
/**
* #return mixed
*/
public function getDomain()
{
return $this->domain;
}
/**
* #param mixed $domain
*/
public function setDomain($domain): void
{
$this->domain = $domain;
}
/**
* #return \DateTime
*/
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
/**
* #return string
*/
public function getId(): string
{
return $this->id;
}
/**
* #param string $id
*/
public function setId(string $id): void
{
$this->id = $id;
}
}
Company embed document:
<?php
namespace App\Document\Entity;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/** #MongoDB\EmbeddedDocument * */
class Company
{
/**
* #MongoDB\Field(type="string",name="company_id")
**/
private string $companyId;
/**
* #MongoDB\Field(type="int",name="company_version")
**/
private int $companyVersion;
/**
* #MongoDB\Field(type="string",name="company_name")
**/
private string $companyName;
/**
* #MongoDB\EmbedOne(targetDocument=Address::class)
*/
private Address $address;
/**
* #MongoDB\Field(type="string",name="email_address")
**/
private string $emailAddress;
/**
* #MongoDB\Field(type="string",name="phone_number")
**/
private string $phoneNumber;
/**
* #MongoDB\Field(type="string",name="website_url")
**/
private string $websiteUrl;
/**
* #MongoDB\Field(type="date",name="creation_date")
**/
private \DateTime $creationDate;
public function __construct()
{
$this->address = new Address();
}
public function getCompanyId(): string
{
return $this->companyId;
}
public function setCompanyId($companyId)
{
$this->companyId = $companyId;
}
public function getCompanyVersion(): int
{
return $this->companyVersion;
}
public function setCompanyVersion($companyVersion)
{
$this->companyVersion = $companyVersion;
}
public function getCreationDate(): \DateTime
{
return $this->creationDate;
}
public function setCreationDate($creationDate)
{
$this->creationDate = $creationDate;
}
public function getWebsiteUrl(): string
{
return $this->websiteUrl;
}
public function setWebsiteUrl($websiteUrl)
{
$this->websiteUrl = $websiteUrl;
}
public function getPhoneNumber(): string
{
return $this->phoneNumber;
}
public function setPhoneNumber($phoneNumber)
{
$this->phoneNumber = $phoneNumber;
}
public function getEmailAddress(): string
{
return $this->emailAddress;
}
public function setEmailAddress($emailAddress)
{
$this->emailAddress = $emailAddress;
}
public function getAddress(): Address
{
return $this->address;
}
public function setAddress(Address $address)
{
$this->address = $address;
}
public function getCompanyName(): string
{
return $this->companyName;
}
public function setCompanyName($companyName)
{
$this->companyName = $companyName;
}
}
What you're trying to do is perfectly fine from ODM's perspective - you can have as many embeddables deep as you want. Unless there is something fishy going on in your Address or Coordinates embedded documents I would expect a bug in the laminas-hydrator package. The fact that ORM does not allow nested embeddables makes this scenario more likely. Best would be to try creating a failing test case and send a pull request with it.
In the meantime you can leverage ODM's hydrator:
$hydrator = $this->dm->getHydratorFactory()->getHydratorFor(Account::class);
$hydrator->hydrate(new Account(), $data, [Query::HINT_READ_ONLY => true]);
Please note the Query::HINT_READ_ONLY passed as a 3rd argument to hydrate. With this hint ODM will not mark hydrated objects as managed which is what you need for later insertion. Hydrated EmbedOne objects should be good to go without the hint, but EmbedMany may not work correctly without it. Please see https://github.com/doctrine/mongodb-odm/issues/1377 and https://github.com/doctrine/mongodb-odm/pull/1403 for more details about said hint.
Thank you #malarzm. I do this:
$coordinates = new Coordinates();
$hydrator->hydrate($data['company']['address']['coordinates'], $coordinates);
unset($data['company']['address']['coordinates']);
$address = new Address();
$hydrator->hydrate($data['company']['address'], $address);
unset($data['company']['address']);
$company = new Company();
$hydrator->hydrate($data['company'], $company);
unset($data['company']);
$account = new Account();
$hydrator->hydrate($data, $account);
$address->setCoordinates($coordinates);
$company->setAddress($address);
$account->setCompany($company);
I realized unset() for each embed sud-document and finally I set each sub-document with set method of my entity.
It was the only way to do work fine.

Doctrine - custom column type not getting updated

I have an entity Dish with a custom column type stock_collection.
<?php
declare(strict_types=1);
namespace App\Entity\Dish;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Resource\Model\ResourceInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Webmozart\Assert\Assert as WebmozartAssert;
/**
* #ORM\Entity
*/
class Dish implements ResourceInterface
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* #ORM\Column(type="string", length=20, unique=true, nullable=false)
*
* #Assert\Length(min="3", max="20")
* #Assert\NotBlank
*/
private string $code;
/**
* #var Collection<int,Stock>
*
* #Assert\Valid
*
* #ORM\Column(type="stock_collection")
*/
private Collection $stocks;
public function __construct($code, $stock)
{
$this->code = $code;
$this->stocks = new ArrayCollection(
array_map(
fn (int $quantity, string $code): Stock => new Stock($code, $quantity),
$stock,
array_keys($stock)
)
);
}
public function getCode(): string
{
return $this->code;
}
public function setCode(string $code): void
{
$this->code = $code;
}
public function getId(): ?int
{
return $this->id;
}
public function getStocks(): Collection
{
return $this->stocks;
}
public function setStocks(Collection $stocks): void
{
$this->stocks = $stocks;
}
public function decreaseStockQuantity(string $menuType, int $quantity): void
{
WebmozartAssert::greaterThanEq($quantity, 0, 'Can not decrease of a negative quantity');
$this->getStock($menuType)->decreaseQuantity($quantity);
}
public function getStockQuantity(string $menuType): int
{
return $this->getStock($menuType)->getQuantity();
}
public function getStock(string $menuType): Stock
{
foreach ($this->stocks as $stock) {
if ($stock->getMenuType() === $menuType) {
return $stock;
}
}
throw new \InvalidArgumentException(sprintf('Stock for menu type %s not found', $menuType));
}
}
<?php
declare(strict_types=1);
namespace App\Entity\Dish;
use Symfony\Component\Validator\Constraints as Assert;
use Webmozart\Assert\Assert as WebmozartAssert;
class Stock implements \JsonSerializable
{
/**
* #Assert\NotBlank
*/
private string $menuType;
/**
* #Assert\NotBlank
* #Assert\PositiveOrZero
*/
private int $quantity;
private bool $inventory;
public function __construct(string $menuType, int $quantity = 0, bool $inventory = true)
{
$this->menuType = $menuType;
$this->quantity = $quantity;
$this->inventory = $inventory;
}
public static function fromArray(array $data): self
{
WebmozartAssert::keyExists($data, 'menuType');
WebmozartAssert::keyExists($data, 'quantity');
WebmozartAssert::keyExists($data, 'inventory');
return new self(
$data['menuType'],
$data['quantity'],
$data['inventory']
);
}
public function getMenuType(): string
{
return $this->menuType;
}
public function setMenuType(string $menuType): void
{
$this->menuType = $menuType;
}
public function getQuantity(): int
{
return $this->quantity;
}
public function setQuantity(int $quantity): void
{
$this->quantity = $quantity;
}
public function inventory(): bool
{
return $this->inventory;
}
public function setInventory(bool $inventory): void
{
$this->inventory = $inventory;
}
public function decreaseQuantity(int $quantity): void
{
$this->quantity -= $quantity;
}
public function jsonSerialize()
{
return [
'menuType' => $this->menuType,
'quantity' => $this->quantity,
'inventory' => $this->inventory,
];
}
}
<?php
declare(strict_types=1);
namespace App\Doctrine\DBAL\Types;
use App\Entity\Dish\Stock;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\JsonType;
final class StockCollectionType extends JsonType
{
public function getName()
{
return 'stock_collection';
}
/**
* #param $value
*
* #throws ConversionException
*
* #return array|mixed|null
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$value = parent::convertToPHPValue($value, $platform);
$value = array_values(array_map(fn ($data): Stock => Stock::fromArray($data), $value));
return new ArrayCollection($value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return parent::convertToDatabaseValue($value->toArray(), $platform);
}
}
When I call the setStocks method to Dish entity, the convertToDatabaseValue method is call correctly and the value is saved in database, but when I edit only a single property of stock, for example calling the descreaseStockQuantity, the method convertToDatabaseValue is not called, so the value doesn't change in database. Why this happens?
That's because Dish (UnitOfWork exactly) doesn't know about changes in Stock. As I remember the easiest way to solve this is to change some updated_at field of Dish in decreaseStockQuantity method. Or you can make Stock immutable and reassign new value on every stock change.

Zend Expressive API does not return contents of objects

I'm creating a small API, mostly for learning purposes, but, I might implement it into a project I'm working on. So far, I have installed the zend expressive skeleton application and set up my models and entities. I'm able to query the database and get results, but, when I return the results as a JSON Response, I can only see a list of empty arrays for each result. I would like to be able to return the actual objects that are being returned from the database instead of converting them to arrays.
HomePageHandler.php
<?php
declare(strict_types=1);
namespace App\Handler;
use App\Entity\Product;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router;
use Zend\Expressive\Template\TemplateRendererInterface;
use App\Model\ProductModel;
class HomePageHandler implements RequestHandlerInterface
{
/** #var string */
private $containerName;
/** #var Router\RouterInterface */
private $router;
/** #var null|TemplateRendererInterface */
private $template;
private $productModel;
public function __construct(
string $containerName,
Router\RouterInterface $router,
?TemplateRendererInterface $template = null,
ProductModel $productModel
) {
$this->containerName = $containerName;
$this->router = $router;
$this->template = $template;
$this->productModel = $productModel;
}
public function handle(ServerRequestInterface $request) : ResponseInterface
{
$data = $this->productModel->fetchAllProducts();
return new JsonResponse([$data]);
//return new HtmlResponse($this->template->render('app::home-page', $data));
}
}
I'm expecting a JSON Response returned with a list of 18 "Product" entities. My results look like.
[[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]]
Let me know if there is any other code you would like to see.
Thanks in advance!
Edited with Product.php code
<?php
/**
* Created by PhpStorm.
* User: Brock H. Caldwell
* Date: 3/14/2019
* Time: 4:04 PM
*/
namespace App\Entity;
class Product
{
protected $id;
protected $picture;
protected $shortDescription;
protected $longDescription;
protected $dimensions;
protected $price;
protected $sold;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getPicture()
{
return $this->picture;
}
/**
* #param mixed $picture
*/
public function setPicture($picture)
{
$this->picture = $picture;
}
/**
* #return mixed
*/
public function getShortDescription()
{
return $this->shortDescription;
}
/**
* #param mixed $shortDescription
*/
public function setShortDescription($shortDescription)
{
$this->shortDescription = $shortDescription;
}
/**
* #return mixed
*/
public function getLongDescription()
{
return $this->longDescription;
}
/**
* #param mixed $longDescription
*/
public function setLongDescription($longDescription)
{
$this->longDescription = $longDescription;
}
/**
* #return mixed
*/
public function getDimensions()
{
return $this->dimensions;
}
/**
* #param mixed $dimensions
*/
public function setDimensions($dimensions)
{
$this->dimensions = $dimensions;
}
/**
* #return mixed
*/
public function getPrice()
{
return $this->price;
}
/**
* #param mixed $price
*/
public function setPrice($price)
{
$this->price = $price;
}
/**
* #return mixed
*/
public function getSold()
{
return $this->sold;
}
/**
* #param mixed $sold
*/
public function setSold($sold)
{
$this->sold = $sold;
}
public function exchangeArray($data)
{
$this->id = (!empty($data['id'])) ? $data['id'] : null;
$this->picture = (!empty($data['picture'])) ? $data['picture'] : null;
$this->shortDescription = (!empty($data['shortDescription'])) ? $data['shortDescription'] : null;
$this->longDescription = (!empty($data['longDescription'])) ? $data['longDescription'] : null;
$this->dimensions = (!empty($data['dimensions'])) ? $data['dimensions'] : null;
$this->price = (!empty($data['price'])) ? $data['price'] : null;
$this->sold = (!empty($data['sold'])) ? $data['sold'] : null;
}
}
You need to either make the properties public, or implement the JsonSerializable interface in your Product entity. All of its properties are protected, which is fine, but that means they aren't exposed when the object is JSON encoded.
Here are some brief examples:
class Example1 { protected $a = 1; }
class Example2 { public $b = 2; }
class Example3 implements JsonSerializable {
protected $c = 3;
public function jsonSerialize() {
return ['c' => $this->c];
}
}
echo json_encode([new Example1, new Example2, new Example3]);
The result:
[{},{"b":2},{"c":3}]
If you choose to implement JsonSerializable, exactly how you do it is up to you, but you just need a jsonSerialize() method that returns the properties you want in the JSON result in a format accessible to json_encode (an object with public properties or an array).

symfony2: Nested sets using RecursiveIteratorIterator

Im having problems following this tutorial: https://wildlyinaccurate.com/simple-nested-sets-in-doctrine-2
This is my code. As you can see in the comments, I can not iterate..
<?php
class Item
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Item", mappedBy="parent", cascade={"remove"})
* #ORM\OrderBy({"position" = "ASC"})
*/
private $children;
/**
* #Gedmo\SortableGroup
* #ORM\ManyToOne(targetEntity="Item", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;
...
///////////////////////////////////////
namespace Project\BackendBundle\Entity;
use Doctrine\Common\Collections\Collection;
class RecursiveCategoryIterator implements \RecursiveIterator
{
private $_data;
public function __construct(Collection $data)
{
$this->_data = $data;
}
public function hasChildren()
{
return ( ! $this->_data->current()->getChildren()->isEmpty());
}
public function getChildren()
{
return new RecursiveCategoryIterator($this->_data->current()->getChildren());
}
public function current()
{
return $this->_data->current();
}
public function next()
{
$this->_data->next();
}
public function key()
{
return $this->_data->key();
}
public function valid()
{
return $this->_data->current() instanceof Project\BackendBundle\Entity\Item;
}
public function rewind()
{
$this->_data->first();
}
}
/////////////////////////////////
$repository = $this->getDoctrine()->getRepository('ProjectBackendBundle:Item');
$root_categories = $repository->findBy(array('parent' => null));
var_dump(count($root_categories));// this returns 2
$collection = new \Doctrine\Common\Collections\ArrayCollection($root_categories);
$category_iterator = new RecursiveCategoryIterator($collection);
$recursive_iterator = new \RecursiveIteratorIterator($category_iterator);
var_dump($recursive_iterator->hasChildren()); //returns true
foreach ($recursive_iterator as $index => $child_category)
{
die("jfks"); //this is not shown
}
What is shown in this article is not nested set.
The best way to treat any kind of trees in symfony2 is to implement doctrine-extensions tree.
Please run one of those examples:
https://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/tree.md
The article that you linked is showing simple adjacent set and is applicable to any kind of application, not only Symfony. But in Symfony2 with Doctrine2 you can depend on doctrineExtensionBundle.

zf2 doctrine odm collection hydration

Hi everybody Im using doctrine ODM and have trouble with hydrator. I can't make extract on document with embed collection nor reference Class. the extract result for these class give me object and i really need to have them in array for rest module which is consumed by backbone implementation.
Here a example class :
Analyse.php Document
<?php
namespace Application\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\Common\Collections\Collection;
/**
* Application\Document\Analyse
*
* #ODM\Document(collection="analyse")
*/
class Analyse extends BaseDocument
{
/**
* #ODM\Id
*/
protected $id;
/**
* #ODM\Field(type="string")
*/
protected $nom;
/**
* #ODM\Field(type="string")
* #ODM\Index
*/
protected $alias;
/**
* #ODM\EmbedMany(targetDocument="ElementsAnalyse")
*/
protected $elements = array();
public function __construct()
{
parent::__construct();
$this->elements = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function setNom($nom)
{
$this->nom = $nom;
return $this;
}
public function getNom()
{
return $this->nom;
}
public function setAlias($alias)
{
$this->alias = $alias;
return $this;
}
public function getAlias()
{
return $this->alias;
}
public function addElements(Collection $elements)
{
foreach ($elements as $element) {
$this->elements->add($element);
}
}
public function removeElements(Collection $elements)
{
foreach ($elements as $item) {
$this->elements->removeElement($item);
}
}
public function getElements()
{
return $this->elements;
}
}
ElementAnalyse.php Collection
<?php
namespace Application\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\Common\Collections\Collection;
/**
* Application\Document\Valeurnormales
*
* #ODM\EmbeddedDocument
*
*/
class ElementsAnalyse
{
/**
* #ODM\Field(type="string")
*/
protected $nom;
/**
* #ODM\Field(type="string")
*/
protected $unite;
/**
* #ODM\EmbedMany(targetDocument="ValeurNormales")
*/
protected $valeurnormales = array();
/**
* #ODM\Field(type="string")
*/
protected $type;
public function __construct()
{
$this->valeurnormales = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Set nom
*/
public function setNom($nom)
{
$this->nom = $nom;
return $this;
}
/**
* Get nom
*/
public function getNom()
{
return $this->nom;
}
/**
* Set unite
*/
public function setUnite($unite)
{
$this->unite = $unite;
return $this;
}
/**
* Get unite
*
* #return string
*/
public function getUnite()
{
return $this->unite;
}
/**
* add valeurnormales
*/
public function addValeurnormales(Collection $vn)
{
foreach ($vn as $item) {
$this->valeurnormales->add($item);
}
}
public function removeValeurnormales(Collection $vn)
{
foreach ($vn as $item) {
$this->valeurnormales->removeElement($item);
}
}
/**
* Get valeurnormales
*/
public function getValeurnormales()
{
return $this->valeurnormales;
}
/**
* Set type
*
* #param $type
* #return Analyse
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* Get type
*
* #return Type
*/
public function getType()
{
return $this->type;
}
/**
* toArray function
*/
public function toArray()
{
return get_object_vars($this);
}
/**
* fromArray function
*
*/
public function fromArray(array $array)
{
$objects = $this->toArray();
foreach($array as $item => $value)
{
if(array_key_exists($item, $objects))
{
$this->$item = $value;
}
}
}
}
Here my getList Method
public function getList()
{
$hydrator = new DoctrineHydrator($entityManager, 'Application\Document\Analyse');
$service = $this->getAnalyseService();
$results = $service->findAll();
$data = $hydrator->extract($results);
return new JsonModel($data);
}
And obviously var_dump($data['elements']) return object Collection or proxy class
Can You help me. Anything will be appreciated it been 2 weeks i can't make it work.
Read about Hydrator Strategy out there but i don't knnow how to implement it.
Currently, the Doctrine ODM implementation does not provide recursion for embedded objects and references.
If you use var_dump() on your $hydrator->extract($results), you'll see that all your embeds/references are there in their original object format.
What you can do here is to use Zend\Stdlib\Hydrator\Strategy, and define your own logic for extraction/hydration. Doctrine extends Zend Framework 2's hydrators and strategies.

Categories