AtroCore implements the Repository design pattern to provide a centralized, abstracted layer for data access operations. This pattern separates business logic from data storage concerns, making your code more maintainable, testable, and flexible.

Key Benefits

  • Separation of Concerns: Business logic stays separate from data access code
  • Testability: Easy to mock repositories for unit testing
  • Flexibility: Switch between different data sources without changing business logic
  • Consistency: Standardized data access patterns across your application
  • Reusability: Share common queries and operations across different parts of your application

Automatic Repository Creation

AtroCore automatically creates repository instances - no manual setup required! Eeach entity gets a default repository that extends \Atro\Core\Templates\Repositories{Type} where {Type} is the entity type , it can be either Base, Hierarchy, Relation or ReferenceData.

To learn more about these types check here.

Accessing a Repository

To get the repository of an entity we need to use the Entity Manager from the container.

/** @var \Espo\Core\ORM\EntityManager $entityManager */
$entityManager = $container->get('entityManager');

/** @var \Atro\Core\Templates\Repositories\Base $repository */
$repository = $entityManager->getRepository('Manufacturer');

// Alternative: Get repository through entity type
$userRepository = $entityManager->getRepository('User');
$productRepository = $entityManager->getRepository('Product');

Repository vs EntityManager Methods

AtroCore provides both repository methods and EntityManager shortcuts:

<?php

// Repository method (explicit)
$manufacturer = $entityManager->getRepository('Manufacturer')->get($id);

// EntityManager shortcut (convenient)
$manufacturer = $entityManager->getEntity('Manufacturer', $id);

// Both approaches are valid - use what fits your coding style

CRUD Operations

Create Operations

Creating New Entities

<?php

/** @var \Espo\Core\ORM\EntityManager $entityManager */
$entityManager = $container->get('entityManager');

// Method 1: Repository approach
/** @var \Atro\Core\Templates\Entities\Base $manufacturer */
$manufacturer = $entityManager->getRepository('Manufacturer')->get();
$manufacturer->set('name', 'ACME Manufacturing');
$manufacturer->set('description', 'Leading manufacturer of quality products');
$manufacturer->set('isActive', true);

$entityManager->getRepository('Manufacturer')->save($manufacturer);

// Method 2: EntityManager shortcut
$manufacturer = $entityManager->getEntity('Manufacturer');
$manufacturer->set('name', 'Global Industries');
$manufacturer->set('isActive', true);

$entityManager->saveEntity($manufacturer);

Bulk Creation

<?php

$manufacturerData = [
    ['name' => 'Company A', 'isActive' => true],
    ['name' => 'Company B', 'isActive' => true],
    ['name' => 'Company C', 'isActive' => false]
];

$repository = $entityManager->getRepository('Manufacturer');

foreach ($manufacturerData as $data) {
    $manufacturer = $repository->get();
    $manufacturer->set($data);
    $repository->save($manufacturer);
}

Read Operations

Single Entity Retrieval

<?php

$entityManager = $container->get('entityManager');

// Read existing entity by ID
$manufacturer = $entityManager->getRepository('Manufacturer')->get($id);

// EntityManager shortcut
$manufacturer = $entityManager->getEntity('Manufacturer', $id);

// Handle non-existent entities
if ($manufacturer) {
    echo "Found: " . $manufacturer->get('name');
} else {
    echo "Manufacturer not found";
}

Advanced Single Entity Queries

<?php

$repository = $entityManager->getRepository('Manufacturer');

// Find first entity matching criteria
$activeManufacturer = $repository
    ->where(['isActive' => true])
    ->findOne();

// Find with specific field selection
$manufacturer = $repository
    ->select(['id', 'name', 'isActive'])
    ->where(['name' => 'ACME Manufacturing'])
    ->findOne();

Update Operations

<?php

$entityManager = $container->get('entityManager');

// Load, modify, and save
$manufacturer = $entityManager->getEntity('Manufacturer', $id);

if ($manufacturer) {
    $manufacturer->set('name', 'Updated Company Name');
    $manufacturer->set('description', 'Updated description');

    // Repository method
    $entityManager->getRepository('Manufacturer')->save($manufacturer);

    // Or EntityManager shortcut
    $entityManager->saveEntity($manufacturer);
}

Delete Operations

AtroCore uses soft deletes by default - entities are marked as deleted but remain in the database:

<?php

$entityManager = $container->get('entityManager');

$manufacturer = $entityManager->getEntity('Manufacturer', $id);

if ($manufacturer) {
    // Soft delete - marks entity as deleted
    $entityManager->getRepository('Manufacturer')->remove($manufacturer);

    // Or using EntityManager shortcut
    $entityManager->removeEntity($manufacturer);
}

Restore Deleted Entities

<?php

$entityManager = $container->get('entityManager');

// Restore a soft-deleted entity
$entityManager->getRepository('Manufacturer')->restore($id);

// Verify restoration
$manufacturer = $entityManager->getEntity('Manufacturer', $id);
if ($manufacturer) {
    echo "Manufacturer successfully restored";
}

Permanent Delete (Use with Caution)

<?php

$entityManager = $container->get('entityManager');

// Permanently remove from database - IRREVERSIBLE!
$entityManager->getRepository('Manufacturer')->deleteFromDb($id);

// This completely removes the record and cannot be undone

deleteFromDb() permanently removes data. Use only when you're certain the data should never be recovered.


 $manufacturer = $entityManager->getEntity('Manufacturer', $id);
 $relationNames = 'products';
 $products = $entityManager->getRepository('Manufacturer')->findRelated($entity, $relationNames);

Querying

Comparison operators

Supported comparison operators are: >, <, >= <=, =, !=.

$stocks = $entityManager->getRepository('Stock')->where(['amount>=' => 150])->find();

When using = use can omit it ['amount' => 50] instead of ['amount=' => 50].

IN and NOT IN

Here the operators =, != are still used, the value just need to be and array.

 $manufacturers = $entityManager->getRepository('Manufacturer')->where(['name' => ['ManufacturerName1', 'ManufacturerName2']]);
 $notInManufacturers = $entityManager->getRepository('Manufacturer')->where(['name!=' => ['ManufacturerName1', 'ManufacturerName2']]);

LIKE operators

* is used for LIKE and !* for NOT LIKE

 $manufacturers = $entityManager->getRepository('Manufacturer')->where(['name*' => '%atrocore%' ]);

OR, AND operators

$opportunityList = $entityManager
    ->getRepository('Product')
    ->where([
        [
            'OR' => [
                ['status' => 'draft'],
                ['isActive' => false],
            ],
            'AND' => [
                'quantity>' => 100,
                'quantity<=' => 999,
            ],
        ]
    ])
    ->findOne();

Custom Repository Classes

Creating a Custom Repository

When you need to add custom query methods or modify default behavior, create a custom repository class:

File: Repositories/Manufacturer.php

<?php

namespace ExampleModule\Repositories;

use Atro\Core\Templates\Repositories\Base;
use Espo\ORM\EntityCollection;
use Espo\ORM\Entity;

class Manufacturer extends Base
{
    /**
     * Get all active manufacturers ordered by name
     *
     * @return EntityCollection
     */
    public function getActiveManufacturers(): EntityCollection
    {
        return $this
            ->where(['isActive' => true])
            ->order('name', 'ASC')
            ->find();
    }

    /**
     * Find manufacturers by name pattern with fuzzy matching
     *
     * @param string $namePattern
     * @return EntityCollection
     */
    public function findByNamePattern(string $namePattern): EntityCollection
    {
        // Clean and prepare pattern
        $pattern = '%' . strtolower(trim($namePattern)) . '%';

        return $this
            ->where(
                [
                    'name*' => $pattern,
                    'isActive' => true
                ]
            )
            ->order('name', 'ASC')
            ->find();
    }

    /**
     * Lifecycle hook: before saving entity
     *
     * @param Entity $entity
     * @param array $options
     */
    protected function beforeSave(Entity $entity, array $options = []): void
    {
        // Standardize name formatting
        if (!empty($entity->get('name'))) {
            $name = trim($entity->get('name'));
            $entity->set('name', ucwords(strtolower($name)));
        }

        // Auto-generate slug from name
        if (!empty($entity->get('name')) && empty($entity->get('slug'))) {
            $slug = strtolower(preg_replace('/[^A-Za-z0-9-]+/', '-', $entity->get('name')));
            $entity->set('slug', trim($slug, '-'));
        }

        parent::beforeSave($entity, $options);
    }

    /**
     * Lifecycle hook: after saving entity
     *
     * @param Entity $entity
     * @param array $options
     */
    protected function afterSave(Entity $entity, array $options = []): void
    {
        parent::afterSave($entity, $options);

        // Clear relevant caches
        $this->getDataManager()->clearCache();

    }
}

Available Lifecycle Hooks

Custom repositories can override these lifecycle methods:

Here is the completed table without any bold formatting.

Method Description When Called
beforeSave() Before entity is saved Create and update operations
afterSave() After entity is saved After successful save
beforeRemove() Before entity deletion Before soft delete
afterRemove() After entity deletion After soft delete
beforeRestore() Before entity restoration Before restoring deleted entity
afterRestore() After entity restoration After successful restoration
beforeRelate() Before entities are related Before a link is established between two entities
afterRelate() After entities are related After a successful link is established
beforeUnrelate() Before entities are unrelated Before a link between two entities is removed
afterUnrelate() After entities are unrelated After a successful link removal

Using Custom Repository Methods

<?php

/** @var \ExampleModule\Repositories\Manufacturer $repository */
$repository = $entityManager->getRepository('Manufacturer');

// Use custom methods
$activeManufacturers = $repository->getActiveManufacturers();

$searchResults = $repository->findByNamePattern('acme');

// Lifecycle hooks are automatically called
$manufacturer = $entityManager->getEntity('Manufacturer');
$manufacturer->set('name', 'new company name');  // Will be formatted by beforeSave()
$repository->save($manufacturer);  // Triggers beforeSave() and afterSave()

Raw SQL Queries

For complex queries beyond ORM capabilities, use direct database connections:

/** @var \Espo\Core\ORM\EntityManager $entityManager */
$entityManager = $containter->get('entityManager');

/** @var \Doctrine\DBAL\Connection $connection */
$connection = $entityManager->getConnection();

$activeManufacturers = $connection->createQueryBuilder()
    ->from($connection->quoteIdentifier('manufacturer'))
    ->select('id, name')
    ->where('deleted = :false')
    ->andWhere('isActive = :true')
    ->setParameter('false', false, \Doctrine\DBAL\ParameterType::BOOLEAN)
    ->setParameter('true', true, \Doctrine\DBAL\ParameterType::BOOLEAN)
    ->fetchAllAssociative();

This method is not recommended because it directly manipulates the database. Any future database schema changes will break the code. Furthermore, this approach bypasses AtroCore's event system. As a result, other parts of the application will not be notified of any changes done, which can lead to data inconsistencies and unexpected behavior.

Repository Limitations and SelectManager

The standard repository pattern in AtroCore has limitations when dealing with relational queries. For advanced querying needs, use the SelectManager:

Repository Limitations

  • Limited support for complex JOIN operations
  • Difficult to query across multiple related entities
  • No support for advanced aggregations
  • Limited subquery capabilities

SelectManager Advantages

  • Advanced JOIN operations
  • Complex WHERE conditions across relationships
  • Aggregation functions (COUNT, SUM, AVG, etc.)
  • Subquery support