Installation
composer require nti/sync-bundle "dev-master"
Ensure your project uses Symfony 2.x/3.x (last release was 2021).
Register the Bundle
Add to AppKernel.php:
new NTI\SyncBundle\NTISyncBundle(),
Define a Syncable Entity
Annotate an entity with @NTI\SyncEntity and implement SyncRepositoryInterface:
use Doctrine\ORM\Mapping as ORM;
use NTI\SyncBundle\Annotation\NTI\SyncEntity;
/**
* @SyncEntity
* @ORM\Entity(repositoryClass="AppBundle\Repository\MyEntitySyncRepository")
*/
class MyEntity {
// ...
}
Configure Sync Mapping
Define a SyncMapping for your entity (e.g., in a service or config):
# config.yml
nti_sync:
mappings:
my_entity:
class: AppBundle\Entity\MyEntity
identifier: id
fields: [name, description]
Update Schema & Routes
php app/console doctrine:schema:update --force
Add to routing.yml:
nti_sync:
resource: "@NTISyncBundle/Resources/config/routing.yml"
Initialize Sync State
Run the provided SQL snippet to populate nti_sync_state for each mapping.
First Sync Trigger Call the sync endpoint (e.g., via CLI or HTTP):
php app/console nti:sync:run --mapping=my_entity
Product entity with @SyncEntity.SyncRepositoryInterface to handle sync logic (e.g., fetch from API, merge changes).SyncMapping for Product in config.yml./sync/run?mapping=product).Annotation-Driven Setup
@SyncEntity on entities to auto-register them for sync./**
* @SyncEntity
* @ORM\Entity
*/
class User { ... }
Repository Integration
SyncRepositoryInterface to define sync behavior:
class UserSyncRepository extends ServiceEntityRepository implements SyncRepositoryInterface {
public function findForSync(SyncMapping $mapping) {
// Fetch unsynced or changed records.
}
public function sync(SyncMapping $mapping, $data) {
// Merge data into entity.
}
}
Parent-Child Sync
@SyncParent on ManyToOne fields to update parent timestamps:
/**
* @ManyToOne(targetEntity="Category")
* @SyncParent(getter="getCategory")
*/
private $category;
Mapping Configuration
config.yml or via YAML/XML:
nti_sync:
mappings:
user:
class: AppBundle\Entity\User
identifier: id
fields: [username, email, last_sync]
Triggering Syncs
php app/console nti:sync:run --mapping=user/sync/run with mapping=user parameter.0 3 * * * php app/console nti:sync:run --mapping=user).External API Sync
findForSync() to fetch only changed records (e.g., via last_updated field).public function findForSync(SyncMapping $mapping) {
return $this->createQueryBuilder('u')
->where('u.lastUpdated > :lastSync')
->setParameter('lastSync', $mapping->getLastSyncTimestamp())
->getQuery()
->getResult();
}
Conflict Resolution
sync() to handle conflicts (e.g., prioritize local DB or external source):
public function sync(SyncMapping $mapping, $data) {
$entity = $this->find($data['id']);
if ($entity->getVersion() > $data['version']) {
// Local version is newer; skip or merge.
return;
}
// Apply changes.
}
Bulk Sync Optimization
sync() to reduce queries:
$em = $this->getEntityManager();
$em->beginTransaction();
foreach ($data as $item) {
$entity = $this->find($item['id']);
$entity->setName($item['name']);
$this->save($entity, $em);
}
$em->commit();
Logging
public function sync(SyncMapping $mapping, $data) {
$this->logger->info('Syncing entity', ['mapping' => $mapping->getName(), 'data' => $data]);
// ...
}
Testing
SyncRepositoryInterface in tests:
$repository = $this->createMock(SyncRepositoryInterface::class);
$repository->method('findForSync')->willReturn([]);
$this->entityManager->expects($this->any())
->method('getRepository')
->with('AppBundle:User')
->willReturn($repository);
Missing @SyncEntity Annotation
@SyncEntity are ignored during sync.public function process(ContainerBuilder $container) {
$entities = $this->findAnnotatedEntities();
foreach ($entities as $entity) {
if (!class_exists($entity) || !in_array(SyncEntity::class, class_uses($entity))) {
throw new \RuntimeException("Entity $entity must be annotated with @SyncEntity");
}
}
}
Uninitialized SyncState
nti_sync_state lacks entries for mappings.SyncMapping configurations are defined.Circular Dependencies in Parent-Child Sync
@SyncParent on bidirectional relationships can cause infinite loops.getter to explicitly define the parent field:
@SyncParent(getter="getParentEntity") // Avoids ambiguity.
Repository Not Implemented
SyncRepositoryInterface not implemented for annotated entities.ServiceEntityRepository and implement the interface:
class MyEntitySyncRepository extends ServiceEntityRepository implements SyncRepositoryInterface {
// Implement required methods.
}
Timestamp Drift
last_sync timestamps may desync if clocks drift between systems.findForSync():
->where('u.lastSync < :now')
->setParameter('now', new \DateTime('now', new \DateTimeZone('UTC')))
Enable SQL Logging
Add to config.yml:
doctrine:
dbal:
logging: true
profiling: true
Check logs for generated queries during sync.
Dump Sync Data
Override findForSync() to log fetched data:
public function findForSync(SyncMapping $mapping) {
$data = $this->createQueryBuilder('e')->getQuery()->getResult();
file_put_contents('sync_debug.log', print_r($data, true));
return $data;
}
Check Sync State
Query nti_sync_state to verify timestamps:
SELECT * FROM nti_sync_state WHERE mapping_id = (SELECT id FROM nti_sync_mapping WHERE name = 'user');
Validate Mappings
Ensure SyncMapping configurations match entity fields:
$mapping = $container->get('nti_sync.mapping.user');
var_dump($mapping->getFields()); // Should match entity properties.
How can I help you explore Laravel packages today?