1. Go to this page and download the library: Download maxkain/eav-bundle library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
maxkain / eav-bundle example snippets
namespace App\Entity\Product\Attribute;
use App\Repository\Product\Attribute\MyAttributeRepository;
use Doctrine\ORM\Mapping as ORM;
use Maxkain\EavBundle\Contracts\Entity\EavAttributeInterface;
#[ORM\Entity(repositoryClass: MyAttributeRepository::class)]
#[ORM\Table('product_my_attribute')]
class MyAttribute implements EavAttributeInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private string $name;
public function __toString(): string
{
return $this->name;
}
// ...getters and setters
}
namespace App\Entity\Product\Attribute;
use App\Entity\Product\Product;
use App\Repository\Product\Attribute\MyEavRepository;
use Doctrine\ORM\Mapping as ORM;
use Maxkain\EavBundle\Contracts\Entity\EavAttributeInterface;
use Maxkain\EavBundle\Contracts\Entity\EavInterface;
use Maxkain\EavBundle\Contracts\Entity\EavEntityInterface;
#[ORM\Entity(repositoryClass: MyEavRepository::class)]
#[ORM\Table('product_my_eav')]
#[ORM\UniqueConstraint(fields: ['entity', 'attribute', 'value'])]
class MyEav implements EavInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(Product::class, inversedBy: 'myEavs')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Product $entity;
#[ORM\ManyToOne(MyAttribute::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private MyAttribute $attribute;
#[ORM\Column]
private string $value;
public function __toString(): string
{
return $this->attribute->getName();
}
}
namespace App\Entity\Product\Attribute;
use App\Repository\Product\Attribute\MyValueRepository;
use Doctrine\ORM\Mapping as ORM;
use Maxkain\EavBundle\Contracts\Entity\EavValueInterface;
#[ORM\Entity(repositoryClass: MyValueRepository::class)]
#[ORM\Table(name: 'product_my_attribute_value')]
#[ORM\UniqueConstraint(fields: ['attribute', 'title'])]
class MyValue implements EavValueInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(MyAttribute::class, inversedBy: 'values')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private MyAttribute $attribute;
#[ORM\Column]
private string $title;
public function __toString(): string
{
return $this->title;
}
}
/**
* @var Collection<MyEav>
*/
#[Assert\Valid]
#[ORM\OneToMany(MyEav::class, 'entity', cascade: ['persist'], orphanRemoval: true)]
private Collection $myEavs;
public function __construct()
{
$this->myEavs = new ArrayCollection();
}
public function getMyEavs(): Collection
{
return $this->myEavs;
}
use Maxkain\EavBundle\Inverter\EavInverterInterface;
use Maxkain\EavBundle\Inverter\EavMultipleInputItem;
use Maxkain\EavBundle\Options\EavOptions;
//...
class MyService
{
public function __construct(
private EavInverterInterface $eavInverter,
) {
}
public myMethod()
{
// ...receive your $entity
// then, define your data:
$myItems = [
new EavMultipleInputItem(
attribute: 1,
values: ['test1', 'test2']
),
new EavMultipleInputItem(
attribute: 2,
values: ['test3', 'test4']
)
];
// also, you can use arrays:
$myItems = [
[
'attribute': 1,
'values': ['test1', 'test2']
],
[
'attribute': 2,
'values': ['test3', 'test4']
]
];
// ...or pass value ids or `MyValue` entities, if you use enum values
// set options:
$options = new EavOptions(
eavFqcn: MyEav::class,
entityFqcn: Product::class,
attributeFqcn: MyAttribute::class,
valueFqcn: MyValue::class, // if you have enum value
multiple: true
);
// and call the inverter:
$eavInverter = $this->eavInverter;
$eavInverter->invert($entity, $myItems, $entity->getMyEavs(), $options);
if (!$eavInverter->isValid()) {
$violations = $eavInverter->getViolations();
// ...set the response with violations
return $response;
}
$em->flush();
// ...set the response
return $response;
}
}
new EavOptions(
reversePropertyMapping: new ReversePropertyMapping(
attribute: 'any_attribute_name',
values: 'any_values_name'
)
);
new EavOptions(
propertyMapping: new PropertyMapping(
entity: 'product',
entityId: 'guid'
// and others...
)
);
new EavOptions(
entityInputType: 'integer',
attributeInputType 'integer',
valueInputType: 'integer'
);
namespace App\Eav;
class ProductConfigurator implements EavConfiguratorInterface
{
/**
* @return array<int|string, EavOptionsInterface>
*/
public function configure(): array
{
return [
new EavOptions(
eavFqcn: MyEav::class,
entityFqcn: Product::class,
attributeFqcn: MyAttribute::class,
valueFqcn: MyValue::class, // if you have enum value
multiple: true
)
];
}
}
use Maxkain\EavBundle\Options\EavOptionsRegistry
//...
public function __construct(
private EavOptionsRegistry $optionsRegistry;
) {
$options = $optionsRegistry->get(MyEav::class);
// or pass the string as option parameter to inverter or converter
$eavInverter->invert($entity, $myItems, $entity->getMyEavs(), MyEav::class);
}
namespace App\Entity\Product\Attribute;
use App\Entity\Product\Category;
use App\Repository\Product\Attribute\MyTagRepository;
use Doctrine\ORM\Mapping as ORM;
use Maxkain\EavBundle\Contracts\Entity\Tag\EavAttributeTagInterface;
#[ORM\Entity(repositoryClass: MyTagRepository::class)]
#[ORM\Table(name: 'product_my_attribute_tag')]
#[ORM\UniqueConstraint(fields: ['attribute', 'tag'])]
class MyTag implements EavAttributeTagInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(MyAttribute::class, inversedBy: 'tags')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private MyAttribute $attribute;
#[ORM\ManyToOne(Category::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Category $tag;
/**
* @var bool Maybe you want to add any additional properties
*/
#[ORM\Column]
private bool $showInFilter = true;
}
public function getEavTags(string $tagKey): iterable
{
return match ($tagKey) {
Category::class => isset($this->category) ? [$this->category] : [],
default => []
};
}
public function getEavTags(string $tagKey): iterable
{
return match ($tagKey) {
Category::class => $this->categories,
default => []
};
}
new EavOptions(
// ...
tagFqcn: Category::class,
attributeTagFqcn: MyTag::class
propertyMapping: new PropertyMapping( // If you have different name of your category field
entityTag: 'category', //for ManyToOne
entityTags: 'categories' //for ManyToMany
// and see others...
)
),
use Doctrine\ORM\EntityManagerInterface;
use Maxkain\EavBundle\Bridge\Doctrine\Query\EavQueryFactory;
use Maxkain\EavBundle\Query\EavExpression;
use Maxkain\EavBundle\Query\EavComparison;
class MyService
{
public function __construct(
private EntityManagerInterface $em,
private EavQueryFactory $eavQueryFactory
) {
}
public function myMethod(): array
{
$qb = $this->em->getRepository(Product::class)->createQueryBuilder('e')->select();
// attribute id => values (or value ids for enum EAV)
$this->eavQueryFactory->addEavFilters($qb, 'e', MyEav::class, [
777 => [111, 222],
888 => [333, new EavComparison('>', 10)]
// ...
]);
$this->eavQueryFactory->addEavFilters($qb, 'e', MyAnotherEav::class, [
999 => 'myValue1',
555 => 'myValue2'
111 => new EavComparison('>', 10),
333 => new EavComparison('LIKE', 'myValue%'),
222 => new EavExpression(':fieldTitle LIKE '. $qb->createNamedParameter('myValue%'))
444 => new EavExpression(':fieldTitle > 18 AND :field < 30')
777 => new EavExpression(':field IN (' . $qb->createNamedParameter([111, 222, 333]) . ')')
// ...
]);
$result = $qb->getQuery()->getResult();
// ...do something with result
return $result;
}
}
// attribute id => values
$myEavFilter = [
22 => [212, 220]
];
// selects the value or value title (if enum) of attribute 22
// if there are multiple values, then MIN(:value) is selected by default
$eavQueryFactory->addValueSelect($qb, 'e', 22, MyEav::class);
// you may customize `select` and `as` expressions
// `as` expression by default has doctrine's option `HIDDEN`, so, the value is selected, but not hydrated
// 'attributeName' placeholder generates automatically, but you can change it
$eavQueryFactory->addValueSelect($qb, 'e', 22, MyEav::class,
// default values:
select: 'MIN(:value)',
as: 'HIDDEN :attributeName'
);
// the query will be like this:
// 'SELECT e, (SELECT MIN(:value) FROM ...) AS HIDDEN :attributeName FROM ...'
// get the generated attribute name,
// it looks like 'attribute_0_22', where 0 is eav options index
$attribute22Name = $eavQueryFactory->getAttributeName(MyEav::class, 22);
// add order by it as by regular column
$qb->addOrderBy($attribute22Name, 'ASC');
//or simply use any other attribute name
$eavQueryFactory->addValueSelect($qb, 'e', 22, MyEav::class,
select: 'MAX(:value)',
as: 'HIDDEN myColumn'
);
$qb->addOrderBy('myColumn', 'DESC');
// restricts selected values to values from the list $myEavFilter[22]
$eavQueryFactory->addValueSelect($qb, 'e', 22, MyEav::class, $myEavFilter[22]);
namespace App\Controller\Admin\Product;
//...
use Maxkain\EavBundle\Bridge\EasyAdmin\EavFieldFactory;
class ProductCrudController extends AbstractCrudController
{
public function __construct(
private EavFieldFactory $eavFieldFactory,
) {
}
public function configureFields(string $pageName): iterable
{
return [
$this->eavFieldFactory->create('myEavs', 'My attributes', MyEav::class),
$this->eavFieldFactory->create('anotherMyEavs', 'Another attributes', AnotherMyEav::class)
//...
];
}
}
public function configureAssets(): Assets
{
$assets = parent::configureAssets();
$assets->addCssFile('bundles/maxkaineav/styles/compact_ea_collection.css');
return $assets;
}
public function configureCrud(): Crud
{
$crud = parent::configureCrud();
$crud->setFormThemes(['@Eav/easy-admin/theme.html.twig', '@EasyAdmin/crud/form_theme.html.twig']);
return $crud;
}