Symfony 5 Save Object in MongoDB Collection - symfony

I have never worked with MongoDb and I am also new to symfony and ODM. I have a collection called userlist. In userlist I want to save multiple books. The book Id's get selected on a Form which I send to the server, in my controller I want to use all the selected BookIds use find($id) and save the returned Book details in my Userlist.
Document\Userlist.php
class Userlist{
/**
* #MongoDB\Id
*/
private $id;
/**
* #MongoDB\Field(type="string")
*/
private $name;
/**
* #MongoDB\Field(type="collection")
*/
private $books;
}
public function __construct()
{
$this->books = new ArrayCollection();
}
public function getBooks(): ?array
{
return array_unique($this->books);
}
public function setBooks(array $books): self
{
$this->books= $books;
return $this;
}
MyController.php
foreach ($_POST['books'] as $bookObjectId) {
$bookData= $dm->getRepository(Book::class)->find($bookObjectId);
$userlist->setBooks($bookData);
$dm->persist($userlist);
$dm->flush();
}
If I use above I get Collection type requires value of type array or null, Doctrine\Common\Collections\ArrayCollection given. So if I take $this->books = new ArrayCollection(); out this error disappears. Unfortunately all I get saved to the document is
books
[0]
No data is saved. If I use
$bookData= $dm->getRepository(Book::class)->find($bookObjectId);
$books = array('_id' => $bookData->getId(),
'name' =>$bookData->getBookName());
$userlist->setBooks($bookData);
$dm->persist($userlist);
$dm->flush();
Then the data will be all saved, but I would like to just get my 1 Document and save it straight into another document without re-creating an Array with calling all the getters and then save that. I have tried to read all over the internet, but I just don't get it. I also tried
public function addBooks(Book $books)
{
$this->books[] = $books;
return $this;
}
but again no luck. The Form by the way has got no BookId form field. Instead I create this dynamic with jQuery, but I doubt that this is a problem. Can anyone point in the right direction by any chance, this would be brilliant. Thank you very much.
UPDATE
My repository file:
public function findReturnArray($id)
{
return $this->createQueryBuilder()
->hydrate(false)
->field('_id')->equals($id)
->getQuery()
->execute()->toArray();
}
In the controller:
foreach ($_POST['bookIds'] as $orderObjectId) {
$bookData[] = $dm->getRepository(Book::class)->findReturnArray($bookObjectId);
}
$userlist->setBooks($bookData);
$dm->persist($userlist);
That does save both books into books but I have 1 issue. It is saved like this
books
[] [0]
{}[0]
bookId
bookName etc
[] [1]
{}[0]
bookId
bookName
what I would like is:
books
{}[0]
bookId
bookName etc
{}[1]
bookId
bookName
setBooks is still the same but I took the $this->books = new ArrayCollection(); out. any idea how I can stop this now?

If you want to persist references to Book documents in your Userlist document, you have to use the ReferenceMany annotation (see docs).
class Userlist
{
// ...
/**
* #MongoDB\ReferenceMany(targetDocument="My\Namespace\Book", storeAs="id")
*/
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
public function getBooks(): array
{
return $this->books->getValues();
}
public function addBook(Book $book)
{
if ($this->books->contains($book)) {
return;
}
$this->books->add($book);
}
}
foreach ($_POST['books'] as $bookObjectId) {
$book = $dm->getRepository(Book::class)->find($bookObjectId);
$userlist->addBook($book);
}
$dm->persist($userlist);
$dm->flush();
UPDATE
After reading your update, I guess this is the change you are looking for:
public function findReturnArray($id)
{
return $this->createQueryBuilder()
->hydrate(false)
->field('_id')->equals($id)
->getQuery()
->getSingleResult();
}

Related

OneToMany relationship issues

I have two entities, PartenairePermission and StructurePermission, I'm trying to get properties from the other entity in a One To Many relationship.
Which was working great with the one to one relationship but I modified it for a One To Many relationship and now I can't access the properties anymore
Attempted to call an undefined method named "setIsMembersRead" of class "Doctrine\ORM\PersistentCollection".
The idea of the script below is when the property is modified in PartenairePermission, it modified the property in StructurePermission too.
Any idea on how to solve this issue?
PartenaireController: [EDITED]
#[Route('/{id}/activate-permission', name: 'app_partenaire_activate-permission', methods: ['GET', 'POST'])]
public function activatePermission(EntityManagerInterface $entityManager, Request $request, PartenaireRepository $partenaireRepository, MailerInterface $mailer): Response
{
$partenairePermission = $entityManager->getRepository(PartenairePermission::class)->findOneBy([ // get the id of the partenaire
'id' => $request->get('id'),
]);
$partenairePermission->setIsMembersRead(!$partenairePermission->isIsMembersRead()); // set the value of the permission to the opposite of what it is ( for toggle switch )
$structurePermission = $partenairePermission->getPermissionStructure();
foreach ($structurePermission as $structurePermission) {
$structurePermission->setIsMembersRead($partenairePermission->isIsMembersRead());
}
$entityManager->persist($partenairePermission);
$entityManager->flush();
PartenairePermission.php :
#[ORM\OneToMany(mappedBy: 'permission_partenaire', targetEntity: StructurePermission::class, orphanRemoval: true)]
private Collection $permission_structure;
public function __construct()
{
$this->permission_structure = new ArrayCollection();
} /**
* #return Collection<int, StructurePermission>
*/
public function getPermissionStructure(): Collection
{
return $this->permission_structure;
}
public function addPermissionStructure(StructurePermission $permissionStructure): self
{
if (!$this->permission_structure->contains($permissionStructure)) {
$this->permission_structure->add($permissionStructure);
$permissionStructure->setPermissionPartenaire($this);
}
return $this;
}
public function removePermissionStructure(StructurePermission $permissionStructure): self
{
if ($this->permission_structure->removeElement($permissionStructure)) {
// set the owning side to null (unless already changed)
if ($permissionStructure->getPermissionPartenaire() === $this) {
$permissionStructure->setPermissionPartenaire(null);
}
}
return $this;
}
StructurePermission.php :
#[ORM\ManyToOne(fetch: "EAGER", inversedBy: 'permission_structure')]
#[ORM\JoinColumn(nullable: false)]
private ?PartenairePermission $permission_partenaire = null;
public function getPermissionPartenaire(): ?PartenairePermission
{
return $this->permission_partenaire;
}
public function setPermissionPartenaire(?PartenairePermission $permission_partenaire): self
{
$this->permission_partenaire = $permission_partenaire;
return $this;
}
Now you have to work different since you changed the association type:
$structurePermission = $partenairePermission->getPermissionStructure();
this will return a Collection (instead of a single Object as with your former One-to-One relationship).
and then something like:
foreach($structurePermission as $permission) {
// here you call your set/get/is for an Object within the Collection
}

In Symfony, how do you fill a new column with values related to existing column values?

Let's say I have a database and a table called User, which is filled up all with "name", "age". Name is like "Peter David Smith" or "Adam Pitt". Inside the User entity, I want to create a new column.
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $name;
/**
* #ORM\Column(type="integer")
*/
private $age
// THIS IS THE NEW COLUMN
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $monogram;
How do I tell Doctrine that the new column called "monogram" should be filled up with the first letters of the $name's words? So in the case of "Peter Daniel Smith" it should be "PDS", at "Adam Pitt" -> "AD". The database has a lot or records with "name" and "age" so the database is not empty.
I've tried this in the constructor, doesn't work:
public function __construct()
{
// ...
$words = explode(" ", $this->name);
foreach ($words as $w){
$this->monogram .= $w[0];
}
}
After this I make the migration, run it, but the monogram column is null everywhere.
Is there any way to fill the $monogram based on the existing database data on this level?
First update current records via a migration file. You have already created the monogram field.
Create a new migration file with doctrine:migrations:generate. This will be located /migrations/VersionXXXXXXXXXXXXXX.php.
Edit your migration file, so it is like this:
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class VersionXXXXXXXXXXXXXX extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
foreach ($this->connection->fetchAll('SELECT id, name FROM user') as $user) {
$this->addSql(
'UPDATE user SET monogram = :monogram_field WHERE id = :user_id',
array(
'monogram_field' => $this->generateInitials($user['name']),
'user_id' => $user['id']
)
);
}
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
}
private function generateInitials(?string $string): ?string
{
if (null === $string || empty($string)) return null;
// Regex to match first letter of each word
preg_match_all('/(?<=\b)[a-z]/i', $string, $matches);
return strtoupper(implode('', $matches[0]));
}
}
You can run doctrine:migrations:migrate or migrations:execute --up 'DoctrineMigrations\VersionXXXXXXXXXXXXXX' to run it.
Now for future insertions, update your User entity to include these functions:
/**
* Gets triggered only on insert
* #ORM\PrePersist
*/
public function onPrePersist()
{
$this->monogram = $this->generateInitials($this->name);
}
private function generateInitials(?string $string): ?string
{
if (null === $string || empty($string)) return null;
// Regex to match first letter of each word
preg_match_all('/(?<=\b)[a-z]/i', $string, $matches);
return strtoupper(implode('', $matches[0]));
}
Remember to add #ORM\HasLifecycleCallbacks to the top of the class. When a new User is inserted it will populate the monogram field.
I haven't fully tested this. Test it locally first on dummy data and tweak as you see fit.

am new with symfony and i need some advices

I have a problem regarding a Symfony application, I want to take as input the "username" or "id" for my controller , and receive information that is in my table "user" and also 2 other table for example : A user has one or more levels , and also it has points must earn points to unlock a level , I want my dan Home page display the username and the level and extent that it has , I jn am beginner and not come to understand the books symfony that I use, I work with PARALLEL " symfony_book " and " symfony_cook_book " and also tutorial youtube May I blocks , here is the code for my cotroler
"
/**
* #Route("/{id}")
* #Template()
* #param $id=0
* #return array
*/
public function getUserAction($id)
{
$username = $this->getDoctrine()
->getRepository('voltaireGeneralBundle:FosUser')
->find($id);
if (!$username) {
throw $this->createNotFoundException('No user found for id '.$id);
}
//return ['id' => $id,'username' => $username];
return array('username' => $username);
}
and I have to use the relationship among classes
use Doctrine\Common\Collections\ArrayCollection;
class Experience {
/**
* #ORM\OneToMany(targetEntity="FosUser", mappedBy="experience")
*/
protected $fosUsers;
public function __construct()
{
$this->fosUsers = new ArrayCollection();
}
}
and
class FosUser {
/**
* #ORM\ManyToOne(targetEntity="Experience", inversedBy="fosUsers")
* #ORM\JoinColumn(name="experience_id", referencedColumnName="id")
*/
protected $fosUsers;
}
and i have always an error
In Symfony you cannot return an array in Action function!, Action function must always return a Response object...So if you want to return data to browser in Symfony, Action function have to return a string wrapped up in Response object.
In your controller code, to return the array to browser, You can serialize an array to JSON and send it back to browser:
public function getUserAction($id)
{
$username = $this->getDoctrine()
->getRepository('voltaireGeneralBundle:FosUser')
->find($id);
if (!$username) {
throw $this->createNotFoundException('No user found for id '.$id);
}
return new Response(json_encode(array('username' => $username)));
}
I suggest you to read more about HTTP protocol , PHP, and Symfony.

Symfony2 - Setting up tags for a blog

New to Symfony2, can someone give me some advice on how to setup tags for a blog site? I've setup tags as it's own entity and will relate tags to the blog entity via ManyToMany relation.
My question is how would I set this up in twig?
In other words, I have a form to entering a new blog, do I setup a new form for just entering tags? Or is there a way to combine entering tags with the blog creation form?
Tags are just a list of unique strings.
Front : I use Select2, there is a really good tag feature : http://ivaynberg.github.io/select2/. It will take/return a string to the server with each "tag" separated by a comma : tag1,tag2,tag3. You can also configure a web-service to research existing tags.
Back : I create a DataTransformer (http://symfony.com/doc/current/cookbook/form/data_transformers.html) as a service and i inject the entity manager in it :
class TagsTransformer implements DataTransformerInterface
{
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function transform($value)
{
if (null === $value) {
return '';
}
if ($value instanceof Collection) {
return implode(',', array_map(function (Tag $tag) {
return (string)$tag;
}, $value->toArray()));
}
return $value;
}
public function reverseTransform($value)
{
if (empty($value)) {
return null;
}
if (is_string($value)) {
$values = explode(',', $value);
foreach ($values as &$value) {
// Find or create it (create the method)
$value = $this->em->getRepository('MySuperBundle:Tag')->findOrCreate(trim($value));
}
unset($value);
return $values;
}
return $value;
}
}
The goal of this transformer is to :
Transform : take the ArrayCollection of Tags entity from Doctrine, and convert it as a simple comma separated string
Reverse : take a simple comma separated string and convert it as an array of unique Tag entity
We then create a Form for tags (again, as a service, with the data transformer in it) :
class TagsType extends AbstractType
{
private $tagsTransformer;
/**
* #param TagsTransformer $tagsTransformer
*/
public function __construct(TagsTransformer $tagsTransformer)
{
$this->tagsTransformer = $tagsTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer($this->tagsTransformer);
}
public function getParent()
{
return 'text';
}
}
Finally, in your form (blog post form for example), you'll have to use your TagsType new "field".

Symfony - Efficient access control for (dynamic) hierarchical roles

I need some advice on how to handle access control for the following scenario:
Corporation
Has one or many companies
Has one or many ROLE_CORP_ADMIN
Company
Has one or many regions.
Has one or many ROLE_COMPANY_ADMIN.
Region:
Has zero or many stores.
Has one or many ROLE_REGION_ADMIN.
Store:
Has zero or many assets.
Has one or many ROLE_STORE_ADMIN.
Has zero or many ROLE_STORE_EMPLOYEE.
Has zero or many ROLE_STORE_CUSTOMER (many is better).
The application should support many corporations.
My instinct is to create either a many-to-many relationship per entity for their admins (eg region_id, user_id). Depending on performance, I could go with a more denormalized table with user_id, corporation_id, company_id, region_id, and store_id. Then I'd create a voter class (unanimous strategy):
public function vote(TokenInterface $token, $object, array $attributes)
{
// If SUPER_ADMIN, return ACCESS_GRANTED
// If User in $object->getAdmins(), return ACCESS_GRANTED
// Else, return ACCESS_DENIED
}
Since the permissions are hierarchical, the getAdmins() function will check all owners for admins as well. For instance:
$region->getAdmins() will also return admins for the owning company, and corporation.
I feel like I'm missing something obvious. Depending on how I implement the getAdmins() function, this approach will require at least one hit to the db every vote. Is there a "better" way to go about this?
Thanks in advance for your help.
I did just what I posed above, and it is working well. The voter was easy to implement per the Symfony cookbook. The many-to-many <entity>_owners tables work fine.
To handle the hierarchical permissions, I used cascading calls in the entities. Not elegant, not efficient, but not to bad in terms of speed. I'm sure refactor this to use a single DQL query soon, but cascading calls work for now:
class Store implements OwnableInterface
{
....
/**
* #ORM\ManyToMany(targetEntity="Person")
* #ORM\JoinTable(name="stores_owners",
* joinColumns={#ORM\JoinColumn(name="store_id", referencedColumnName="id", nullable=true)},
* inverseJoinColumns={#ORM\JoinColumn(name="person_id", referencedColumnName="id")}
* )
*
* #var ArrayCollection|Person[]
*/
protected $owners;
...
public function __construct()
{
$this->owners = new ArrayCollection();
}
...
/**
* Returns all people who are owners of the object
* #return ArrayCollection|Person[]
*/
function getOwners()
{
$effectiveOwners = new ArrayCollection();
foreach($this->owners as $owner){
$effectiveOwners->add($owner);
}
foreach($this->getRegion()->getOwners() as $owner){
$effectiveOwners->add($owner);
}
return $effectiveOwners;
}
/**
* Returns true if the person is an owner.
* #param Person $person
* #return boolean
*/
function isOwner(Person $person)
{
return ($this->getOwners()->contains($person));
}
...
}
The Region entity would also implement OwnableInterface and its getOwners() would then call getCompany()->getOwners(), etc.
There were problems with array_merge if there were no owners (null), so the new $effectiveOwners ArrayCollection seems to work well.
Here is the voter. I stole most of the voter code and OwnableInterface and OwnerInterface from KnpRadBundle:
use Acme\AcmeBundle\Security\OwnableInterface;
use Acme\AcmeBundle\Security\OwnerInterface;
use Acme\AcmeUserBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class IsOwnerVoter implements VoterInterface
{
const IS_OWNER = 'IS_OWNER';
private $container;
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container) {
$this->container = $container;
}
public function supportsAttribute($attribute)
{
return self::IS_OWNER === $attribute;
}
public function supportsClass($class)
{
if (is_object($class)) {
$ref = new \ReflectionObject($class);
return $ref->implementsInterface('Acme\AcmeBundle\Security\OwnableInterface');
}
return false;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
foreach ($attributes as $attribute) {
if (!$this->supportsAttribute($attribute)) {
continue;
}
if (!$this->supportsClass($object)) {
return self::ACCESS_ABSTAIN;
}
// Is the token a super user? This will check roles, not user.
if ( $this->container->get('security.context')->isGranted('ROLE_SUPER_ADMIN') ) {
return VoterInterface::ACCESS_GRANTED;
}
if (!$token->getUser() instanceof User) {
return self::ACCESS_ABSTAIN;
}
// check to see if this token is a user.
if (!$token->getUser()->getPerson() instanceof OwnerInterface) {
return self::ACCESS_ABSTAIN;
}
// Is this person an owner?
if ($this->isOwner($token->getUser()->getPerson(), $object)) {
return self::ACCESS_GRANTED;
}
return self::ACCESS_DENIED;
}
return self::ACCESS_ABSTAIN;
}
private function isOwner(OwnerInterface $owner, OwnableInterface $ownable)
{
return $ownable->isOwner($owner);
}
}

Resources