Database Layer Architecture Guide
This page is current and ready for use.
Overview
This document outlines the database layer architecture in our application. While we start with four core layers, we may extend this based on application complexity and specific needs.
Core Layers
1. Entity Layer (App\User\Entity
)
Entities represent our domain models and their relationships.
namespace App\User\Entity;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
class User extends BaseEntity implements UserInterface
{
#[ORM\Column(length: 100)]
#[Assert\Length(min: 2, max: 100)]
private string $name;
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserEmail::class)]
private Collection $userEmails;
}
Responsibilities
- Data structure definition
- Relationship mappings
- Basic property getters/setters
- No business logic
- No query logic
2. Repository Layer (App\User\Repository
)
Repositories handle basic CRUD operations and simple queries.
namespace App\User\Repository;
class UserRepository extends ServiceEntityRepository
{
public function save(User $user, bool $flush = true): void
{
$this->getEntityManager()->persist($user);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function findByEmail(string $email): ?User
{
return $this->findOneBy(['email' => $email]);
}
}
Responsibilities
- Basic CRUD operations
- Single entity operations
- Simple queries
- Direct entity management
- Database transaction handling
3. Query Layer (App\User\Query
)
Query services handle complex database queries and optimized data retrieval.
namespace App\User\Query;
class UserFinder
{
public function __construct(
private readonly UserRepository $repository
) {}
public function findActiveUsersInRegion(string $region): array
{
return $this->repository->createQueryBuilder('u')
->innerJoin('u.userStatuses', 'us')
->innerJoin('u.address', 'a')
->where('us.isCurrent = true')
->andWhere('a.region = :region')
->setParameter('region', $region)
->getQuery()
->getResult();
}
}
Responsibilities
- Complex database queries
- Multiple joins and conditions
- Performance optimization
- Specific search criteria
- Bulk data retrieval
4. Collection Layer (App\User\Collection
)
Collections handle in-memory operations on sets of entities.
namespace App\User\Collection;
class UserCollection extends ArrayCollection
{
public function getAdults(): self
{
return $this->filter(function(User $user) {
return $user->getAge() >= 18;
});
}
public function sortByName(): self
{
$users = $this->toArray();
usort($users, fn(User $a, User $b) =>
$a->getName() <=> $b->getName()
);
return new self($users);
}
}
Responsibilities
- In-memory data manipulation
- Filtering fetched data
- Sorting and grouping
- Domain-specific collection behavior
- Chaining operations
Additional Layers (As Needed)
5. Criteria Layer (App\User\Criteria
)
Reusable query criteria for complex filtering.
namespace App\User\Criteria;
class ActiveUserCriteria implements QueryCriteriaInterface
{
public function apply(QueryBuilder $qb): void
{
$qb->innerJoin('user.status', 'us')
->where('us.isActive = :active')
->setParameter('active', true);
}
}
Responsibilities
- Reusable query conditions
- Complex filtering logic
- Query builder modifications
- Search criteria encapsulation
6. Filter Layer (App\User\Filter
)
Specific filters that can be applied to queries.
namespace App\User\Filter;
class UserLocationFilter implements FilterInterface
{
public function apply(QueryBuilder $qb, string $region): void
{
$qb->andWhere('user.region = :region')
->setParameter('region', $region);
}
}
Responsibilities
- Specific filtering logic
- Query parameter handling
- Condition building
- Filter chain management
7. View Layer (App\User\View
)
Read models and DTOs for specific use cases.
namespace App\User\View;
class UserProfileView
{
public function __construct(
public readonly string $id,
public readonly string $name,
public readonly string $primaryEmail,
public readonly string $primaryMobile
) {}
public static function fromEntity(User $user): self
{
return new self(
$user->getId(),
$user->getName(),
$user->getPrimaryEmail()?->getEmail(),
$user->getPrimaryMobile()?->getNumber()
);
}
}
Responsibilities
- Data transfer objects
- Read model definitions
- View-specific data structures
- Entity to DTO mapping
Layer Integration Example
class UserService
{
public function __construct(
private UserRepository $repository,
private UserFinder $finder,
private ActiveUserCriteria $activeCriteria,
private UserLocationFilter $locationFilter
) {}
public function getActiveUsersInRegion(string $region): array
{
// Get users using complex query
$users = $this->finder->findActiveUsersInRegion($region);
// Use collection for in-memory operations
$collection = new UserCollection($users);
$processedUsers = $collection
->getAdults()
->sortByName();
// Convert to view models
return array_map(
fn(User $user) => UserProfileView::fromEntity($user),
$processedUsers->toArray()
);
}
}
When to Add Additional Layers
-
Start with Core Layers:
- Entity
- Repository
- Query
- Collection
-
Add Additional Layers When:
- You see repeated query patterns (Criteria)
- You need reusable filters (Filter)
- You have specific read requirements (View)
- The complexity justifies the separation
Benefits of This Architecture
-
Separation of Concerns
- Clear responsibilities
- Maintainable code
- Easy testing
-
Scalability
- Easy to extend
- Supports complex requirements
- Flexible structure
-
Performance
- Optimized queries
- Efficient data handling
- Controlled data loading
-
Code Organization
- Clear structure
- Logical grouping
- Easy to understand
Best Practices
-
Keep It Simple
- Start with core layers
- Add complexity only when needed
- Document layer responsibilities
-
Maintain Consistency
- Follow naming conventions
- Use consistent patterns
- Keep similar logic together
-
Consider Performance
- Use appropriate layer for operations
- Optimize queries where needed
- Balance flexibility and efficiency
-
Think About Scale
- Design for growth
- Plan for extensions
- Consider team collaboration