I'm using MongoDB with Doctrine for a Symfony project.
For a document (I call it class Product) I need to define a field (e.g. name), but I want to act doctrine in a way that the document cannot be saved without defining a it. Meaning, whenever creating or updating a new Product, the property $name has to be defined.
For doctrine and MySql this is quite simple. There you have to say:
class Product {
...
/** #ORM\Column(type="string", nullable=false) */
private $name;
...
}
Is there a similar way in MongoDb?
The following code (which I found in a similar thread) doesn't work:
class Product {
...
/**
* #MongoDB\Field(type="string")
* #Constraints\NotBlank
*/
protected $name;
...
}
So far we've been struggling with Symfony, Doctrine, and Serializer depth.
I'd like to be able to provide just one-level-depth JSON REST API with Symfony, allowing me to manage my "foreign key" and relation logic directly from the view.
GET /people/1
{
id:1,
name:"theonewhoknocks",
friends: [3, 12, 25]
}
Using FosRESTBundle, we've been strugling at succeeding on that. (we've seen "depth" anotations and "groups" views for models, but none of this fit our need).
The question is simple, before we make a choice for our future API, we have to know:
is api-platform able to provide a dead simple one level (with apparent foreign keys) REST API ?
API Platform can handle that using the Serializer Symfony bundle and its annotation set.
To define what will be returned by an operation we use a normalizationContext which define group(s) of property to include in result of an api operation. Property to include have then this group name linked to #Groups serializer annotation
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity()
* #ApiResource(normalizationContext={"groups"={"read"}}
*/
class Book {
/**
* #ORM\Column()
* #Groups({"read"})
*/
private $title;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="books")
* #Groups({"read"})
*/
private $author;
/**
* Will not be included in result
*/
private $secret_comment;
}
If a relation column is in a Group as $author here, properties defined in a group in the child class will be included in the result
/**
* #ORM\Entity()
* #ApiResource(normalizationContext={"groups"={"read"}})
*/
class User {
/**
* #ORM\Column()
* #Groups({"read"})
*/
private $username;
}
In order to avoid cyclic recursion you can specify the max depth of child relation joins with annotation #MaxDepth(n) where n is the max depth (1 in your case). This annotation has to be enabled with enable_max_depth property in serializer context of the #ApiResource annotation
/**
* #ApiPlatform(normalizationContext={"groups"={"read"}, "enable_max_depth"=true})
*/
class Book {
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="books")
* #Groups({"read"})
* #MaxDepth(1)
*/
private $author;
}
Please note that API Platform is in this case an agregation of existing bundles and features. Refer to the main bundles for detailed informations (here the Symfony Serializer bundle)
Be aware that the MaxDepth on the symfony serializer might not behave as expected, see https://github.com/symfony/symfony/issues/33466
The annotation basically says "from here on, render max N instances of THE SAME CLASS into the graph"
So given a pseudo structure like
Class A:
#MaxDepth(1)
Class B:
Class C:
Class D:
would render the whole thing A.B.C.D, while
Class A:
#MaxDepth(1)
Class B:
Class B:
Class B:
would only render A.B.B
Which is quite different from what e.g. JMS serializer is doing, where MaxDepth really means "From here on, max N steps into the relation graph".
Bad thing is that JMS serializer is not supported by api-platform: https://github.com/api-platform/api-platform/issues/753
So the answer to your question, atm, is: no. :/
As #leberknecht indicated, you might not get the results you are looking for using MaxDepth.
This script:
#[ApiResource(
normalizationContext: ['enable_max_depth'=>true],
)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 180)]
private ?string $someProperty = null;
#[ORM\ManyToOne(targetEntity: UserClass::class)]
#[SymfonyMaxDepth(1)]
private ?User $createdBy = null;
}
will return:
{
"id": 123,
"someProperty": "objectProperty",
"createdBy": {
"id": 20,
"someProperty": "parentProperty",
"createdBy": "users/5"
}
}
and this script:
#[ApiResource]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 180)]
private ?string $someProperty = null;
#[ORM\ManyToOne(targetEntity: UserClass::class)]
#[ApiProperty(readableLink: false, writableLink: false)]
private ?User $createdBy = null;
}
will return
{
"id": 123,
"someProperty": "objectProperty",
"createdBy": "users/20"
}
I have an Entity that pulls it's data from a REST web service. To keep thing consistent with the entities in my app that pull data from the database I have used ORM and overridden the find functions in the repository.
My problem is that ORM seems to demand a database table. When I run doctrine:schema:update it moans about needing an index for the entity then when I add one it creates me a table for the entity. I guess this will be a problem in the future as ORM will want to query the database and not the web service.
So... am I doing this wrong?
1, If I continue to use ORM how can I get it to stop needing the database table for a single entity.
2, If I forget ORM where do I put my data loading functions? Can I connect the entity to a repository without using ORM?
So... am I doing this wrong?
Yes. It doesn't make sense to use the ORM interfaces if you don't really want to use an ORM.
I think the best approach is NOT to think about implementation details at all. Introduce your own interfaces for repositories:
interface Products
{
/**
* #param string $slug
*
* #return Product[]
*/
public function findBySlug($slug);
}
interface Orders
{
/**
* #param Product $product
*
* #return Order[]
*/
public function findProductOrders(Product $product);
}
And implement them with either an ORM:
class DoctrineProducts implements Products
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function findBySlug($slug)
{
return $this->em->createQueryBuilder()
->select()
// ...
}
}
or a Rest client:
class RestOrders implements Orders
{
private $httpClient;
public function __construct(HttpClient $httpClient)
{
$this->httpClient = $httpClient;
}
public function findProductOrders(Product $product)
{
$orders = $this->httpClient->get(sprintf('/product/%d/orders', $product->getId()));
$orders = $this->hydrateResponseToOrdersInSomeWay($orders);
return $orders;
}
}
You can even make some methods use the http client and some use the database in a single repository.
Register your repositories as services and use them rather then calling Doctrine::getRepository() directly:
services:
repository.orders:
class: DoctrineOrders
arguments:
- #doctrine.orm.entity_manager
Always rely on your repository interfaces and never on a specific implementation. In other words, always use a repository interface type hint:
class DefaultController
{
private $orders;
public function __construct(Orders $orders)
{
$this->orders = $orders;
}
public function indexAction(Product $product)
{
$orders = $this->orders->findProductOrders($product);
// ...
}
}
If you don't register controllers as services:
class DefaultController extends Controller
{
public function indexAction(Product $product)
{
$orders = $this->get('repository.orders')->findProductOrders($product);
// ...
}
}
A huge advantage of this approach is that you can always change the implementation details later on. Mysql is not good enough for search anymore? Let's use elastic search, it's only a single repository!
If you need to call $product->getOrders() and fetch orders from the API behind the scenes it should still be possible with some help of doctrine's lazy loading and event listeners.
I used Symfony2 and Doctrine MongoDBBundle and I have simple Single Collection Inheritance classes. How can I know what type of document it is in a twig template? For example the base class is Entity and extended by User and Organization, in listing those in a twig template I'd like to know what type of entity it is (i.e. whether it's a User or an Organization). I wonder if it's possible to get the value of the DiscriminatorField of the document.
/**
* #MongoDB\Document(collection="entity")
* #MongoDB\InheritanceType("SINGLE_COLLECTION")
* #MongoDB\DiscriminatorField(fieldName="type")
* #MongoDB\DiscriminatorMap({"user"="User", "shop"="Shop"})
*/
class Entity
{
/**
* #MongoDB\Id
*/
protected $id;
protected $entityType;
public function getEntityType()
{
return $this->entityType;
}
}
Can anybody tell me whether its posible to override doctrine2 persistentobject magic getters\setters? i'd like to do the below:-
public function setDob($dob)
{
$this->dob= new \Date($date);
}
however my entity is defined as:-
use Doctrine\Common\Persistence\PersistentObject;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="Ajfit\Repository\User")
* #ORM\HasLifecycleCallbacks
*/
class User extends \Doctrine\Common\Persistence\PersistentObject
{
/**
* #var date $dob
*
* #ORM\Column(name="dob", type="date")
*/
protected $dob;
}
the public function setDob does not get called when I create the entity using:-
public function getNewRecord() {
return $this->metadata->newInstance();
}
I get the below error:-
Notice:- array to string conversion ...Doctrine\DBAL\Statement.php on line 98
Any help would be much apprieciated.
Thanks
Andrew
__call of PersistentObject#__call will not be called if you defined the setDob method.
What you're doing there is creating a new instance via metadata. What you are doing there is probably assuming that __construct or any setter/getter should be called by the ORM. Doctrine avoids to call any methods on your object when generating it via metadata/hydration (check ClassMetadataInfo#newInstance to see how it is done) as it does only know it's fields.
This allows you to be completely independent from Doctrine's logic.
About the notice, that is a completely different issue coming from Doctrine\DBAL\Statement, which suggests me that you have probably some wrong parameter binding in a query. That should be handled separately.