feat(db): add entities

This commit is contained in:
Ondrej Vlach 2024-08-02 14:13:25 +02:00
parent 07d7a3025a
commit 857507abe5
No known key found for this signature in database
GPG Key ID: 7F141CDACEDEE2DE
17 changed files with 652 additions and 0 deletions

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Entity\Database\Comments;
use App\Entity\Database\Posts\Post;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: "comments")]
class Comment
{
public function __construct(
#[Id,
Column]
public ?int $id,
#[ManyToOne(targetEntity: Post::class, inversedBy: "comments")]
public Post $post,
#[Column(type: "text")]
public string $name,
#[Column(length: 320 /* see RFC5321 */)]
public string $email,
#[Column(type: "text")]
public string $body,
) {
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Entity\Database\Posts;
use App\Entity\Database\Comments\Comment;
use App\Entity\Database\Users\User;
use App\Repository\Post\PostRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
#[Entity(repositoryClass: PostRepository::class)]
#[Table(name: "posts")]
class Post
{
public function __construct(
#[Id,
Column]
public ?int $id,
#[ManyToOne(targetEntity: User::class)]
public User $user,
#[Column(type: "text")]
public string $title,
#[Column(type: "text")]
public string $body,
/**
* @var Collection<int, Comment> $comments
*/
#[OneToMany(targetEntity: Comment::class, mappedBy: "post")]
public Collection $comments,
) {
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Entity\Database\Users;
use App\Repository\Users\AddressRepository;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table;
#[Entity(repositoryClass: AddressRepository::class)]
#[Table(name: "addresses")]
class Address
{
public function __construct(
#[Id,
Column,
GeneratedValue(strategy: "SEQUENCE")]
public ?int $id,
#[OneToOne(targetEntity: User::class, mappedBy: 'address')]
public ?User $user,
#[Column(type: "text")]
public string $street,
#[Column(type: "text")]
public string $suite,
#[Column(type: "text")]
public string $city,
#[Column(type: "text")]
public string $zipcode,
#[Column]
public float $lat,
#[Column]
public float $lng,
) {
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Entity\Database\Users;
use App\Repository\Users\CompanyRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
#[Entity(repositoryClass: CompanyRepository::class)]
#[Table(name: "companies")]
class Company
{
public function __construct(
#[Id,
Column,
GeneratedValue(strategy: "SEQUENCE")]
public ?int $id,
/**
* @var Collection<int, User> $users
*/
#[OneToMany(targetEntity: User::class, mappedBy: 'company')]
public Collection $users,
// TODO: prepokladam ze tohle muzu povazovat za klic a vsechny dalsi parametry budou stejne pro kazdou company se stejnym jmenem
#[Column(type: "text", unique: true)]
public string $name,
#[Column(type: "text")]
public string $catchPhrase,
#[Column(type: "text")]
public string $bs,
) {
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Entity\Database\Users;
use App\Repository\Users\UserRepository;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\OneToOne;
use Doctrine\ORM\Mapping\Table;
#[Entity(repositoryClass: UserRepository::class)]
#[Table(name: "users")]
class User
{
public function __construct(
#[Id,
Column]
public ?int $id,
#[Column(type: "text")] // TODO: type: text is really non-optimal. But there is no docs for api
public string $name,
#[Column(type: "text")]
public string $username,
#[Column(type: "text")]
public string $email,
#[Column(type: "text")]
public string $phone,
#[Column(type: "text")]
public string $website,
#[OneToOne(targetEntity: Address::class, inversedBy: "user")]
public Address $address,
#[ManyToOne(targetEntity: Company::class, inversedBy: "users")]
public Company $company,
) {
if (empty($this->address->user)) {
$this->address->user = $this;
}
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository\Comments;
use App\Entity\Database\Comments\Comment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Comment>
*/
class CommentsRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Comment::class);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository\Post;
use App\Entity\Database\Posts\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Post>
*/
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Repository\Users;
use App\Entity\Database\Users\Address;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Address>
*/
class AddressRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Address::class);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Repository\Users;
use App\Entity\Database\Users\Company;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Company>
*/
class CompanyRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Company::class);
}
/**
* Find company by name
* @param string $name
* @return Company|null
*/
public function findByName(string $name): ?Company
{
return $this->findOneBy(['name' => $name]);
}
/**
* Delete companies with no associated users
* @return void
*/
public function deleteOrphanRecords(): void
{
$this->getEntityManager()->createQuery('DELETE FROM App\Entity\Database\Users\Company c WHERE NOT EXISTS (SELECT 1 FROM App\Entity\Database\Users\User u WHERE u.company = c.id)')
->execute();
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Repository\Users;
use App\Entity\Database\Users\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*/
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
/**
* Find users by IDs. If user not exists, will not be set in the result array.
* @param array<int> $ids
* @return array<User>
*/
public function findByIds(array $ids): array
{
$result = [];
$resultQ = $this->createQueryBuilder('u')
->where('u.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->getResult();
foreach ($resultQ as $user) {
$result[$user->id] = $user;
}
return $result;
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Tests\Common;
use Doctrine\ORM\EntityManagerInterface;
trait DatabaseTestTrait
{
protected ?EntityManagerInterface $em;
protected function bootDatabase(): void
{
/**
* @var EntityManagerInterface $em
*/
$em = $this->getContainer()->get(EntityManagerInterface::class);
$this->em = $em;
$this->em->getConnection()->executeQuery(<<<EOSQL
DO $$
DECLARE row RECORD;
BEGIN
FOR row IN SELECT table_name
FROM information_schema.tables
WHERE table_type='BASE TABLE'
AND table_schema='public'
LOOP
EXECUTE format('TRUNCATE TABLE %I CASCADE;',row.table_name);
END LOOP;
END;
$$;
EOSQL);
}
protected function getEntityManager(): EntityManagerInterface
{
if ($this->em === null) {
throw new \LogicException('Database has not been booted yet.');
}
return $this->em;
}
protected function getEntityCount(string $entityClass): int
{
return (int) $this->getEntityManager()->createQuery('SELECT COUNT(e) FROM ' . $entityClass . ' e')->getSingleScalarResult();
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Tests\Common;
use Faker\Factory;
use Faker\Generator;
trait FakerTrait
{
private ?Generator $faker;
protected function bootFaker(): void
{
$this->faker = Factory::create();
}
protected function getFaker(): Generator
{
if ($this->faker === null) {
throw new \LogicException('Faker has not been booted yet.');
}
return $this->faker;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Tests\Common\Generators;
use App\Entity\Database\Comments\Comment;
use App\Entity\Database\Posts\Post;
use App\Tests\Common\DatabaseTestTrait;
use App\Tests\Common\FakerTrait;
trait CommentGeneratorTrait
{
use FakerTrait;
use DatabaseTestTrait;
private function createComment(int $id, Post $post): Comment
{
$comment = new Comment(
$id,
$post,
$this->getFaker()->name(),
$this->getFaker()->email(),
$this->getFaker()->text(500)
);
$this->getEntityManager()->persist($comment);
$this->getEntityManager()->flush();
$this->getEntityManager()->refresh($post);
return $comment;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Tests\Common\Generators;
use App\Entity\Database\Posts\Post;
use App\Tests\Common\DatabaseTestTrait;
use App\Tests\Common\FakerTrait;
use Doctrine\Common\Collections\ArrayCollection;
trait PostGeneratorTrait
{
use UserGeneratorTrait;
use FakerTrait;
use DatabaseTestTrait;
private const int POST_GENERATOR_SYSTEM_POST_USER = 1000;
private function createPost(int $id): Post
{
$user = $this->createUser($id + self::POST_GENERATOR_SYSTEM_POST_USER);
$post = new Post(
$id,
$user,
$this->getFaker()->text(500),
$this->getFaker()->text(5000),
new ArrayCollection(),
);
$this->getEntityManager()->persist($post);
$this->getEntityManager()->flush();
return $post;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Tests\Common\Generators;
use App\Entity\Database\Users\Address;
use App\Entity\Database\Users\Company;
use App\Entity\Database\Users\User;
use App\Tests\Common\DatabaseTestTrait;
use App\Tests\Common\FakerTrait;
use Doctrine\Common\Collections\ArrayCollection;
trait UserGeneratorTrait
{
use FakerTrait;
use DatabaseTestTrait;
private function createUser(int $id): User
{
$user = new User(
$id,
$this->getFaker()->name(),
$this->getFaker()->userName(),
$this->getFaker()->email(),
$this->getFaker()->phoneNumber(),
$this->getFaker()->url(),
new Address(
1,
null,
$this->getFaker()->streetAddress(),
(string) $this->getFaker()->randomNumber(),
$this->getFaker()->city(),
$this->getFaker()->postcode(),
$this->getFaker()->randomNumber(),
$this->getFaker()->randomNumber(),
),
new Company(
1,
new ArrayCollection(),
$this->getFaker()->company(),
$this->getFaker()->text(10),
$this->getFaker()->text(100)
)
);
$this->getEntityManager()->persist($user->address);
$this->getEntityManager()->persist($user->company);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->refresh($user->address);
$this->getEntityManager()->refresh($user->company);
$this->getEntityManager()->flush();
return $user;
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Tests\Db\Users;
use App\Entity\Database\Users\Company;
use App\Repository\Users\CompanyRepository;
use App\Tests\Common\DatabaseTestTrait;
use App\Tests\Common\FakerTrait;
use App\Tests\Common\Generators\UserGeneratorTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CompanyRepositoryTest extends KernelTestCase
{
use FakerTrait;
use DatabaseTestTrait;
use UserGeneratorTrait;
public function setUp(): void
{
parent::bootKernel();
$this->bootFaker();
$this->bootDatabase();
$this->getFaker()->unique();
}
public function testFindByNameWillReturnCompany(): void
{
$c1 = new Company(
1,
new ArrayCollection(),
$this->getFaker()->company(),
$this->getFaker()->text(50),
$this->getFaker()->text(100)
);
$this->getEntityManager()->persist($c1);
$this->getEntityManager()->persist(new Company(
2,
new ArrayCollection(),
$this->getFaker()->company(),
$this->getFaker()->text(50),
$this->getFaker()->text(100)
));
$this->getEntityManager()->flush();
/**
* @var CompanyRepository $repository
*/
$repository = parent::getContainer()->get(CompanyRepository::class);
$this->assertNotNull($repository->findByName($c1->name));
$this->assertNull($repository->findByName($this->getFaker()->text()));
}
public function testDeleteOrphanRecordsWillDeleteCompaniesWithoutUsers(): void
{
$c1 = new Company(
1,
new ArrayCollection(),
$this->getFaker()->company(),
$this->getFaker()->text(50),
$this->getFaker()->text(100)
);
$user = $this->createUser(1);
$this->getEntityManager()->persist($c1);
$this->getEntityManager()->persist($user->address);
$this->getEntityManager()->persist($user->company);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
/**
* @var CompanyRepository $repository
*/
$repository = $this->getContainer()->get(CompanyRepository::class);
$repository->deleteOrphanRecords();
$result = $repository->findAll();
$this->assertCount(1, $result);
$this->assertSame($user->company, $result[0]);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Tests\Db\Users;
use App\Repository\Users\UserRepository;
use App\Tests\Common\DatabaseTestTrait;
use App\Tests\Common\FakerTrait;
use App\Tests\Common\Generators\UserGeneratorTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class UserRepositoryTest extends KernelTestCase
{
use FakerTrait;
use DatabaseTestTrait;
use UserGeneratorTrait;
protected function setUp(): void
{
parent::bootKernel();
$this->bootFaker();
$this->bootDatabase();
}
public function testFindByIdsWillReturnOnlyIdsWhichExists(): void
{
$user = $this->createUser(1);
/**
* @var UserRepository $userRepository
*/
$userRepository = parent::getContainer()->get(UserRepository::class);
$result = $userRepository->findByIds([1, /* non existent ids */ 2]);
$this->assertCount(1, $result);
$this->assertSame(1, $result[1]->id);
}
}