Symfony Doctrine two keys for different purposes, one AUTO_INCREMENT - php

I'm new to Symfony and Doctrine and using the latest versions, am trying to create a Model with two key fields (attributes) for different purposes. $id is not the Primary Key but is AUTO_INCREMENT in MySQL/MariaDB. $key is string length=100 mapped to CHAR[100] and is the primary key and #ORM\Id. Even though this is what I'm trying to do, I don't think it's a supported configuration. I want to be able to merge() using key but also want to have the AI column for references (joining) because in part as I think it will have better performance, and even if not I want it anyway :) The thing is the data source which I use to populate may send multiple objects with the same key in which case I want them to persist to the same record, the last object the last to update the db, no duplicate keys.
I read somewhere, to use the DB implementation of AI and on non primary key, I may have to write a custom Hydrator. I have little idea what files to create and where to put them, and what mapping I have to do, or how exactly to do it. Hope someone can help.
<?php
// src/Entity/View.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ViewRepository")
*/
class View
{
/**
* #ORM\Column(type="integer", name="created_by")
*/
private $created_by;
/**
* #ORM\Column(type="integer", name="updated_by")
*/
private $updated_by;
/**
* #ORM\Column(type="bigint", name="id")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\Id
* #ORM\Column(type="string", length=100, name="`key`")
*/
private $key;
/**
* #ORM\Column(type="json", name="`value`")
*/
private $value;
public function getCreatedBy()
{
return $this->created_by;
}
public function setCreatedBy($value)
{
$this->created_by = $value;
}
public function getUpdatedBy()
{
return $this->updated_by;
}
public function setUpdatedBy($value)
{
$this->updated_by = $value;
}
public function getId()
{
return $this->id;
}
public function setId($value)
{
$this->id = $value;
}
public function getKey()
{
return $this->key;
}
public function setKey($value)
{
$this->key = $value;
}
public function getValue()
{
return $this->value;
}
public function setValue($value)
{
$this->value = $value;
}
}
/* View */
CREATE TABLE IF NOT EXISTS `View` (
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by INT UNSIGNED NOT NULL,
updated_by INT UNSIGNED NOT NULL,
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`key` CHAR(100) NOT NULL,
`value` JSON NOT NULL,
PRIMARY KEY PK_VIEW (`key`),
UNIQUE KEY UQ_VIEW (id)
) CHARSET = latin1;
$entity = new View();
$entity->setCreatedBy(0);
$entity->setUpdatedBy(0);
$entity->setKey($messageId);
$entity->setValue($json);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->merge($entity);
$entityManager->flush();

Related

Doctrine Symfony Migration unable to correctly detect custom Tinyint type length in MySql DB

I have defined a custom doctrine tinyint type for my Symfony 6 application like so:
namespace App\DoctrineTypes;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
/**
* Class TinyIntType
*
* #package App\DoctrineTypes
*/
class TinyIntType extends Type
{
protected string $name = 'tinyint';
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return sprintf('TINYINT(%d)', $column['length']);
}
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): int
{
return (int)$value;
}
public function getName(): string
{
return $this->name;
}
public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
return true;
}
}
In my doctrine.yaml I got:
doctrine:
dbal:
types:
tinyint: App\DoctrineTypes\TinyIntType
I then make use of the type in an entity like so:
/**
* #ORM\Column(
* type="tinyint",
* name="retrieveflag",
* length=4,
* options={"default"=0},
* nullable=false
* )
*/
private ?int $retrieve_flag = 0;
After I ran migrations on this for the first time the DB showed the column correctly like so:
`retrieveflag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '(DC2Type:tinyint)',
However, every time I run
php bin/console doctrine:migrations:diff
I get this in my migration:
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE my_table CHANGE retrieveflag retrieveflag TINYINT(4) DEFAULT \'0\' NOT NULL COMMENT \'(DC2Type:tinyint)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE my_table CHANGE retrieveflag retrieveflag TINYINT(0) DEFAULT \'0\' NOT NULL COMMENT \'(DC2Type:tinyint)\'');
}
So even though the DB contains the retrieveflag field at the correct length (4), the migration seems to think it's 0 and wants to change it.
How do I show doctrine that the DB state matches the entity defined length?
I am using:
PHP 8.1
MySql 5.7
Symfony 6.1.6
Doctrine Bundle 2.7.0
Doctrine Migrations Bundle 3.2.2

Fetching data from Symfony Doctrine DB

I have just started my first symfony project and im a little bit confused.
I would like to query database for Shelfs which have no products on them.
I have come up with such sql query which seems to work:
select distinct she.id shelf_id,rac.id rack_id , row.id row_id, war.id warehouse_id from shelfs she
left join products pro on she.id = pro.warehouse_id
left join racks rac on she.rack_id=rac.id
left join rows row on rac.row_id = row.id
left join warehouses war on row.warehouse_id = war.id
where she.id not in (select shelf_id from products);
But i got no idea how to translate it to dql.
I have tried to write such function in Repository:
{
$entityManager = $this->getEntityManager();
$query = $entityManager->createQuery(
'SELECT distinct she
FROM App\Entity\Shelfs she
JOIN App\Entity\Racks rac
JOIN App\Entity\Rows row
JOIN App\Entity\Warehouses war
WHERE she NOT IN (SELECT prod FROM App\Entity\Products prod)
');
return $query->getResult();
}
That is partialy working as at first it showed the exact values that sql query did.
For example 3 Shelf id with out products on them.
But when I added product to one of those shelf it still shows that 3 are empty.
Thats my controller:
public function index()
{
$repository = $this->getDoctrine()->getRepository(Products::class);
$product = $repository->find(76); // that's for testing and it is refreshing
$warehousesRepository = $this->getDoctrine()
->getRepository(Warehouses::class);
$freespace = $warehousesRepository->findempty(); // that is not refreshing
return $this->render('base.html.twig', [
'selected_view' => 'pages/findPlace.html.twig',
'product' => $product,
'freespace' => $freespace
]);
}
Here is my Products Entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductsRepository")
*/
class Products
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer")
*/
private $barcode;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\Column(type="float")
*/
private $price;
/**
* #ORM\Column(type="integer")
*/
private $quantity;
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="Warehouses")
* #ORM\JoinColumn(name="warehouse_id", referencedColumnName="id")
*/
private $warehouse_id;
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="Rows")
* #ORM\JoinColumn(name="row_id", referencedColumnName="id")
*/
private $row_id;
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="Racks")
* #ORM\JoinColumn(name="rack_id", referencedColumnName="id")
*/
private $rack_id;
/**
* #ORM\Column(type="integer")
* #ORM\ManyToOne(targetEntity="Shelfs")
* #ORM\JoinColumn(name="shelf_id", referencedColumnName="id")
*/
private $shelf_id;
public function getId(): ?int
{
return $this->id;
}
public function getBarcode(): ?int
{
return $this->barcode;
}
public function setBarcode(int $barcode): self
{
$this->barcode = $barcode;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getPrice(): ?float
{
return $this->price;
}
public function setPrice(float $price): self
{
$this->price = $price;
return $this;
}
public function getQuantity(): ?int
{
return $this->quantity;
}
public function setQuantity(int $quantity): self
{
$this->quantity = $quantity;
return $this;
}
public function getWarehouseId(): ?int
{
return $this->warehouse_id;
}
public function setWarehouseId(int $warehouse_id): self
{
$this->warehouse_id = $warehouse_id;
return $this;
}
public function getRowId(): ?int
{
return $this->row_id;
}
public function setRowId(int $row_id): self
{
$this->row_id = $row_id;
return $this;
}
public function getRackId(): ?int
{
return $this->rack_id;
}
public function setRackId(int $rack_id): self
{
$this->rack_id = $rack_id;
return $this;
}
public function getShelfId(): ?int
{
return $this->shelf_id;
}
public function setShelfId(int $shelf_id): self
{
$this->shelf_id = $shelf_id;
return $this;
}
}
I tried to make relations after creating entities by adding:
* #ORM\ManyToOne(targetEntity="Warehouses")
* #ORM\JoinColumn(name="warehouse_id", referencedColumnName="id")
but that didn't work so i guess i need to redo my entities and specify relations in the wizzard.
I have made relations using sql:
ALTER TABLE shelfs ADD FOREIGN KEY (rack_id) REFERENCES racks(id);
ALTER TABLE racks ADD FOREIGN KEY (row_id) REFERENCES rows(id);
ALTER TABLE rows ADD FOREIGN KEY (warehouse_id) REFERENCES warehouses(id);
ALTER TABLE products ADD FOREIGN KEY (warehouse_id) REFERENCES warehouses(id);
ALTER TABLE products ADD FOREIGN KEY (rack_id) REFERENCES racks(id);
ALTER TABLE products ADD FOREIGN KEY (row_id) REFERENCES rows(id);
ALTER TABLE products ADD FOREIGN KEY (shelf_id) REFERENCES shelfs(id);
ALTER TABLE order_items ADD FOREIGN KEY (product_id) REFERENCES products(id);
ALTER TABLE order_items ADD FOREIGN KEY (order_id) REFERENCES orders(id);
ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users(id);
It should look something like this:
I feel like i got lost in the process somewhere.
I got no idea what's happening and i cannot find anything related in symfony documentation.
$sel = $this->createQueryBuilder('prod') // FROM App\Entity\Products prod
->select('prod')
// ->from('App\Entity\Products', 'prod'); // FROM App\Entity\Products prod
;
$res = $this->createQueryBuilder('she') // FROM App\Entity\Shelfs she
->select('DISTINCT she')
// ->from('App\Entity\Shelfs', 'she') // FROM App\Entity\Shelfs she
->leftJoin('she.rac', 'rac')
->leftJoin('she.row', 'row')
->leftJoin('she.war', 'war')
->andWhere('she NOT IN :(sel)')->setParameter('sel', $sel->getQuery()->getResult());
;
return $res->getQuery()->getResult();

Is this the common structure for the domain mapper model?

Hopefully i am asking this on the right stack exchange forum. If not please do let me know and I will ask somewhere else. I have also asked on Code Review, but the community seems a lot less active.
As I have self learned PHP and all programming in general, I have only recently found out about 'Data Mappers' which allows data to be passed into classes without said classes knowing where the data comes from. I have read some of the positives of using mappers and why they make it 'easier' to perform upgrades later down the line, however I am really struggling to find out the reccomended way of using mappers and their layouts in a directory structure.
Let's assume we have a simple application whos purpose is to echo out a first name and last name of a user.
The way I have been using/creating mappers (as well as the file structure is as follows):
index.php
include 'classes/usermapper.php';
include 'classes/user.php';
$user = new User;
$userMapper = new userMapper;
try {
$user->setData([
$userMapper->fetchData([
'username'=>'peter1'
])
]);
} catch (Exception $e) {
die('Error occurred');
}
if ($user->hasData()) {
echo $user->fullName();
}
classes/user.php
class User {
private $_data;
public function __construct() { }
public function setData($userObject = null) {
if (!$userObject) { throw new InvalidArgumentException('No Data Set'); }
$this->_data = $dataObject;
}
public function hasData() {
return (!$this->_data) ? false : true;
}
public function fullName() {
return ucwords($this->_data->firstname.' '.$this->_data->lastname);
}
}
classes/usermapper.php
class userMapper {
private $_db;
public function __construct() { $this->_db = DB::getInstance(); }
public function fetchData($where = null) {
if (!is_array($where)) {
throw new InvalidArgumentException('Invalid Params Supplied');
}
$toFill = null;
foreach($where as $argument=>$value) {
$toFill .= $argument.' = '.$value AND ;
}
$query = sprintf("SELECT * FROM `users` WHERE %s ", substr(rtrim($toFill), 0, -3));
$result = $this->_db->query($query); //assume this is just a call to a database which returns the results of the query
return $result;
}
}
With understanding that the users table contains a username, first name and last name, and also that a lot of sanitizing checks are missing, why are mappers convenient to use?
This is a very long winded way in getting data, and assuming that users aren't everything, but instead orders, payments, tickets, companies and more all have their corresponding mappers, it seems a waste not to create just one mapper and implement it everywhere in each class.
This allows the folder structure to look a whole lot nicer and also means that code isn't repeated as often.
The example mappers looks the same in every case bar the table the data is being pulled from.
Therefore my question is. Is this how data mappers under the 'domain model mappers' should look like, and if not how could my code be improved? Secondly is this model needed in all cases of needing to pull data from a database, regardless of the size of class, or should this model only be used where the user.php class in this case is very large?
Thank you in advance for all help.
The Data Mapper completely separates the domain objects from the persistent storage (database) and provides methods that are specific to domain-level operations. Use it to transfer data from the domain to the database and vice versa. Within a method, a database query is usually executed and the result is then mapped (hydrated) to a domain object or a list of domain objects.
Example:
The base class: Mapper.php
abstract class Mapper
{
protected $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
}
The file: BookMapper.php
class BookMapper extends Mapper
{
public function findAll(): array
{
$sql = "SELECT id, title, price, book_category_id FROM books;";
$statement = $this->db->query($sql);
$items = [];
while ($row = $statement->fetch()) {
$items[] = new BookEntity($row);
}
return $items;
}
public function findByBookCategoryId(int $bookCategoryId): array
{
$sql = "SELECT id, title, price, book_category_id
FROM books
WHERE book_category_id = :book_category_id;";
$statement = $this->db->prepare($sql);
$statement->execute(["book_category_id" => $bookCategoryId]);
$items = [];
while ($row = $statement->fetch()) {
$items[] = new BookEntity($row);
}
return $items;
}
/**
* Get one Book by its ID
*
* #param int $bookId The ID of the book
* #return BookEntity The book
* #throws RuntimeException
*/
public function getById(int $bookId): BookEntity
{
$sql = "SELECT id, title, price, book_category_id FROM books
WHERE id = :id;";
$statement = $this->db->prepare($sql);
if (!$result = $statement->execute(["id" => $bookId])) {
throw new DomainException(sprintf('Book-ID not found: %s', $bookId));
}
return new BookEntity($statement->fetch());
}
public function insert(BookEntity $book): int
{
$sql = "INSERT INTO books SET title=:title, price=:price, book_category_id=:book_category_id";
$statement = $this->db->prepare($sql);
$result = $statement->execute([
'title' => $book->getTitle(),
'price' => $book->getPrice(),
'book_category_id' => $book->getBookCategoryId(),
]);
if (!$result) {
throw new RuntimeException('Could not save record');
}
return (int)$this->db->lastInsertId();
}
}
The file: BookEntity.php
class BookEntity
{
/** #var int|null */
protected $id;
/** #var string|null */
protected $title;
/** #var float|null */
protected $price;
/** #var int|null */
protected $bookCategoryId;
/**
* Accept an array of data matching properties of this class
* and create the class
*
* #param array|null $data The data to use to create
*/
public function __construct(array $data = null)
{
// Hydration (manually)
if (isset($data['id'])) {
$this->setId($data['id']);
}
if (isset($data['title'])) {
$this->setTitle($data['title']);
}
if (isset($data['price'])) {
$this->setPrice($data['price']);
}
if (isset($data['book_category_id'])) {
$this->setBookCategoryId($data['book_category_id']);
}
}
/**
* Get Id.
*
* #return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* Set Id.
*
* #param int|null $id
* #return void
*/
public function setId(?int $id): void
{
$this->id = $id;
}
/**
* Get Title.
*
* #return null|string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* Set Title.
*
* #param null|string $title
* #return void
*/
public function setTitle(?string $title): void
{
$this->title = $title;
}
/**
* Get Price.
*
* #return float|null
*/
public function getPrice(): ?float
{
return $this->price;
}
/**
* Set Price.
*
* #param float|null $price
* #return void
*/
public function setPrice(?float $price): void
{
$this->price = $price;
}
/**
* Get BookCategoryId.
*
* #return int|null
*/
public function getBookCategoryId(): ?int
{
return $this->bookCategoryId;
}
/**
* Set BookCategoryId.
*
* #param int|null $bookCategoryId
* #return void
*/
public function setBookCategoryId(?int $bookCategoryId): void
{
$this->bookCategoryId = $bookCategoryId;
}
}
The file: BookCategoryEntity.php
class BookCategoryEntity
{
const FANTASY = 1;
const ADVENTURE = 2;
const COMEDY = 3;
// here you can add the setter and getter methods
}
The table structure: schema.sql
CREATE TABLE `books` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`price` decimal(19,2) DEFAULT NULL,
`book_category_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `book_category_id` (`book_category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE `book_categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*Data for the table `book_categories` */
insert into `book_categories`(`id`,`title`) values (1,'Fantasy');
insert into `book_categories`(`id`,`title`) values (2,'Adventure');
insert into `book_categories`(`id`,`title`) values (3,'Comedy');
Usage
// Create the database connection
$host = '127.0.0.1';
$dbname = 'test';
$username = 'root';
$password = '';
$charset = 'utf8';
$collate = 'utf8_unicode_ci';
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES $charset COLLATE $collate"
];
$db = new PDO($dsn, $username, $password, $options);
// Create the data mapper instance
$bookMapper = new BookMapper($db);
// Create a new book entity
$book = new BookEntity();
$book->setTitle('Harry Potter');
$book->setPrice(29.99);
$book->setBookCategoryId(BookCategoryEntity::FANTASY);
// Insert the book entity
$bookId = $bookMapper->insert($book);
// Get the saved book
$newBook = $bookMapper->getById($bookId);
var_dump($newBook);
// Find all fantasy books
$fantasyBooks = $bookMapper->findByBookCategoryId(BookCategoryEntity::FANTASY);
var_dump($fantasyBooks);

Could not load other mapping of entity in doctrine2 and zend 1

Anyone, please help for me this issue. I'm the newbie of Doctrine. After some time to configure the doctrine(version 2.3) working with zend(version 1.10.8). All working fine except the last step. I have a table "test table" like this
CREATE TABLE IF NOT EXISTS `testtable` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(250) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=13 ;
INSERT INTO `testtable` (`id`, `name`) VALUES
(1, 'test1'),
(2, 'what');
This is the Entity annotations
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table(name="testtable")
* #ORM\Entity
*
*/
class Tallcat_Doctrine_Entity_Test
{
/**
* #ORM\Column(type="string")
*/
private $name;
/**
* #ORM\Id #ORM\Column
* #ORM\GeneratedValue
*/
private $id;
/**
* #param string $name
*/
public function setName($name)
{
$this->name = (string)$name;
}
/**
* #return string
*/
public function getName()
{
return $this->name;
}
}
And in the controller of zend, i call this for testing:
class DoctrineController extends Zend_Controller_Action
{
protected $em = null;
public function init()
{
$this->em = \Zend_Registry::get('doctrine')->getEntityManager();
}
public function indexAction()
{
try{
$test = new Tallcat_Doctrine_Entity_Test();
$testMap = $this->em->getRepository('Tallcat_Doctrine_Entity_Test')- >findAll();
echo '<pre>';
print_r($testMap);
echo '</pre>';
die();
}catch(Exception $ex) {
print_r($ex);die();
}
}
}
And this is the result:
Array
(
[0] => Tallcat_Doctrine_Entity_Test Object
(
[name:Tallcat_Doctrine_Entity_Test:private] =>
[id:Tallcat_Doctrine_Entity_Test:private] => 1
)
[1] => Tallcat_Doctrine_Entity_Test Object
(
[name:Tallcat_Doctrine_Entity_Test:private] =>
[id:Tallcat_Doctrine_Entity_Test:private] => 2
)
)
I don't know what wrong with the file name, it cannot load. I tried to save one record, it can save the Id, except the file Name.
Someone could help please.
I solved it myself. The problem is:
The Doctrine cached the mapping before load, so the old code didn't update. So i restart the computer(stupid way) to clear cache. And it works perfect.
Thanks

doctrine 2 - persisting an entity with composite key

A Doctrine 2 Entity with a composite key:
/**
* #Entity
*/
class Test
{
/**
* #Id
* #Column (type="integer", length=11, name="id")
*
*/
protected $id = null;
/**
* #Id
* #Column (type="integer", length=11, name="idtwo")
*
*/
protected $idtwo = null;
public function setIdTwo($id)
{
$this->idtwo = $id;
}
public function setId($id)
{
$this->id = $id;
}
}
Saving the Entity
$test = new Test();
$test->setId(1);
$test->setIdTwo(1);
$em->persist($test);
DB Table:
CREATE TABLE `Bella_Test` (
`id` int(11) NOT NULL,
`idtwo` int(11) NOT NULL,
PRIMARY KEY (`id`,`idtwo`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Expected result: a row is added to the db table with two id fields, both with a value of 1.
Actual result: No row is added to the db table. No exception is thrown.
Question: What is going on?
You can use a try catch block to see what happens
try{
$em->flush(); //don't forget flush after persisting an object
}
catch(Exception $e){
echo 'Flush Operation Failed: '.$e->getMessage();
}
Other assumption, in my opinion, your entity table name and DB table name may not match each other. I think it's not a bad idea to give a try
/**
* #Entity
* #Table(name="Bella_Test")
*/
.
.
.

Categories