<?php
namespace App\EventSubscriber;
use App\Controller\Api\DocumentManagerController;
use App\Entity\InvoicePaymentStatus;
use App\Entity\InvoicePositions;
use App\Entity\Invoices;
use App\Entity\Partner;
use App\Entity\PartnerCertificateDocuments;
use App\Entity\ProjectOrders;
use App\Entity\ProjectOrderTaskFulfillmentWorkDocs;
use App\Entity\Projects;
use App\Entity\ProjectStakeholders;
use App\Entity\ProjectStatus;
use App\Entity\StockItems;
use App\Entity\Stocks;
use App\Entity\Vendor;
use App\Entity\WorkerActivities;
use App\Entity\WorkerCertificateDocuments;
use App\Entity\Worker;
use App\Entity\WorkerInProject;
use App\Enum\PaymentStatusEnum;
use App\Service\Calculators\ProjectOrderCalculator;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreRemoveEventArgs;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Doctrine\Persistence\Event\LifecycleEventArgs as LifecycleEventArgsAlias;
use Doctrine\ORM\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use App\Entity\InvoiceType;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\UnitOfWork;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Doctrine during load jobs
*/
class DoctrineSubscriber implements EventSubscriberInterface
{
private $serviceArgs;
private ManagerRegistry $managerRegistry;
private TranslatorInterface $translator;
private ProjectOrderCalculator $projectOrderCalculator;
/**
* @deprecated
*/
private PaymentStatusEnum $paymentStatusEnum;
public function __construct(
$args,
ManagerRegistry $managerRegistry,
TranslatorInterface $translator,
ProjectOrderCalculator $projectOrderCalculator,
PaymentStatusEnum $paymentStatusEnum
)
{
$this->serviceArgs = $args;
$this->managerRegistry = $managerRegistry;
$this->translator = $translator;
$this->projectOrderCalculator = $projectOrderCalculator;
$this->paymentStatusEnum = $paymentStatusEnum;
}
// Listen to postLoad event
public static function getSubscribedEvents()
{
/**
* Metrics hesaplama onFlush
* Delete öncesi kontrol preRemove
* DB sonrası işlem (log / event) postFlush
*
* Metrics için önerilmez
* preUpdate|postUpdate|postLoad
*/
return array(
/**
* Entity DB’den yüklendikten hemen sonra
*/
Events::postLoad,
#Events::postUpdate,
/**
* 📍 Ne zaman?
* persist() çağrıldıktan sonra
* flush() öncesi
* Sadece NEW entity için
* 📍 Ne için?
* Default value set
* createdAt / UUID
*/
Events::prePersist,
/**
* 📍 Ne zaman?
* UPDATE edilecek entity için
* flush() öncesi
* 📍 Özel durum:
* ChangeSet zaten hesaplanmış
* Değişiklik yaptıysan:
*/
#Events::preUpdate,
/**
* 📍 Ne zaman?
* remove() çağrıldıktan sonra
* flush() öncesi
* 📍 Ne için?
* Delete öncesi validation
* Loglama
* 🚫 Yapma: başka entity update
*/
Events::preRemove,
/**
* 📍 Ne zaman?
* Doctrine ChangeSet’i hesapladıktan sonra
* SQL yazılmadan hemen önce
* 📍 Ne için? 🔥🔥🔥
* Başka entity’leri manuel olarak update etmek
* $uow->recomputeSingleEntityChangeSet()
*/
Events::onFlush,
/**
* 📍 Ne zaman?
* Tüm SQL’ler çalıştıktan sonra
* Transaction commit edilmeden hemen önce
* 📍 Ne için?
* Side effects
* Queue / mail / log
* Flush çağırmak SERBEST (dikkatli)
* 🚫 Yapma: entity state değiştirme (sonsuz loop riski)
*/
Events::postFlush
);
}
// Call on Update
public function postFlush(PostFlushEventArgs $args){
}
public function preRemove(PreRemoveEventArgs $args){
/*$manager = $args->getObjectManager();
$uow = $manager->getUnitOfWork();
#dd($uow->getScheduledEntityDeletions());
$entity = $args->getObject();
#dd($entity);
// Update Metrics
if( $entity instanceof ProjectOrderUndertakings){
$calculated = $this->projectOrderCalculator
->calculateOrderPayments($entity->getProjectOrder());
#dd($calculated);
$entity->getProjectOrder()->getProjectOrderMetrics()->setTotalUndertakings($calculated['undertakingTotal'] - $entity->getPrice());
}*/
}
// Call on (New & Update & Remove)
public function onFlush(OnFlushEventArgs $args)
{
/***
* $args->getObjectManager() → EntityManager döner
* ❌ Flush edilen entity dönmez
* ❌ instanceof ProjectOrderUndertakings burada anlamsız
* Yani şunlar artık BOŞ:
* getScheduledEntityInsertions()
* getScheduledEntityUpdates()
* getScheduledEntityDeletions()
*/
$manager = $args->getObjectManager();
$uow = $manager->getUnitOfWork();
/**
* @deprecated
*/
$deleted = array_values($uow->getScheduledEntityDeletions());
/**
* ornek entityRemove($entity) busekilde setEntity(null) oldugundan delete yapilmis entity degeri gorunmuyot yani deleted ici bos o yüzden preRemove kullan
*/
/**
* $entities = [...$uow->getScheduledEntityUpdates(), ...$uow->getScheduledEntityInsertions() ];
* // Trigger's
* foreach ($entities as $entity) {
* if ($entity instanceof ProjectOrderUndertakings) {
* * $trigger = new ProjectOrderMetricsTrigger($this->projectOrderCalculator);
* * // $trigger->projectOrderUndertakingChange($uow, $manager, $entity, $deleted);
* * // Only Remove is Manually With OnRemove in owned Service !!
* * // ⚠️Don't use this success over inline ProjectOrderService
* * $trigger->projectOrderUndertakingChange($uow, $manager, $entity, []);
* }
* }
*/
}
// Call on (New)
public function prePersist( PrePersistEventArgs $args)
{
$entity = $args->getObject();
if( $entity instanceof ProjectOrderTaskFulfillmentWorkDocs ){
$entity->setFile($this->serviceArgs['dirs']['taskFulfillmentWorkDocsReadDir'] . DIRECTORY_SEPARATOR . $entity->getName() );
}
}
// Set the services.yaml param on the entity
public function postLoad( LifecycleEventArgs $args)
{
$entity = $args->getObject();
if ($entity instanceof PartnerCertificateDocuments ) {
$entity->setHydrateDocumentsData([
"id" => $entity->getId(),
"file_name" => $entity->getName(),
"file_real_path" => $this->serviceArgs['dirs']['partnerCertFetchDir'] . DIRECTORY_SEPARATOR . $entity->getName(),
"extension" => DocumentManagerController::getFileExtension( $entity->getName() ),
"type" => DocumentManagerController::getFileFullType( DocumentManagerController::getFileExtension( $entity->getName() ) ),
// Access from frontend
"remove_path" => "/api/partner/remove-partner-certificate-history-document/" . $entity->getPartnerCertificateHistory()->getPartnerCertificate()->getPartner()->getSlug() . "/" . DocumentManagerController::Encrypt($entity->getId()),
// Access just backend
"remove_link" => $this->serviceArgs['dirs']['partnerCertManageDir'] . DIRECTORY_SEPARATOR . $entity->getName()
]);
}
if ( $entity instanceof WorkerCertificateDocuments ) {
$entity->setHydrateDocumentsData([
"id" => $entity->getId(),
"file_name" => $entity->getName(),
"file_real_path" => $this->serviceArgs['dirs']['workerCertFetchDir'] . DIRECTORY_SEPARATOR . $entity->getName(),
"extension" => DocumentManagerController::getFileExtension( $entity->getName() ),
"type" => DocumentManagerController::getFileFullType( DocumentManagerController::getFileExtension( $entity->getName() )),
// Access from frontend
"remove_path" => "/api/worker/remove-worker-certificate-history-document/" . $entity->getWorkerCertificateHistory()->getWorkerCertificate()->getWorker()->getSlug() . "/" . DocumentManagerController::Encrypt($entity->getId()),
// Access just backend
"remove_link" =>$this->serviceArgs['dirs']['workerCertManageDir'] . DIRECTORY_SEPARATOR . $entity->getName()
]);
}
if ( $entity instanceof Worker ) {
#dd(12);
// Image Full Path
// $entity->setProfileImageFullPath( $this->serviceArgs['dirs']['workerImagesFetchDir'] . DIRECTORY_SEPARATOR . $entity->getProfileImage() );
// // SECTION “idempotent” Behevior
// $profileImage = $entity->getProfileImage();
// if ($profileImage && !str_starts_with($profileImage, $this->serviceArgs['dirs']['workerImagesFetchDir'])) {
// $entity->setProfileImage($this->serviceArgs['dirs']['workerImagesFetchDir'] . DIRECTORY_SEPARATOR . $profileImage);
// }
/**
* SECTION Worker için sadece runtime path ekle
* // README
* 1️⃣ Doctrine UnitOfWork “dirty check” yapıyor
* postLoad sırasında sen Worker entity’sinde setProfileImage() cagirdim.
* Doctrine bunu entity’nin değiştiği olarak algılar ve next flush’ta DB’ye yazılacak olarak işaretledi eget bu entity guncellemsini istemesem bile sirf bu yüzden worker guncellencek .
* postLoad içinde set yaptığımda, eğer o entity UnitOfWork tarafından izleniyorsa ve flush çağrılırsa, Doctrine bunu kaydediyor benide öldürüyor
* Gunvenli yol olarak runtime Path eklemeyi yaptim
* */
$this->setWorkerImagePathRuntime($entity);
// $entity->setProfileImage( $this->serviceArgs['dirs']['workerImagesFetchDir'] . DIRECTORY_SEPARATOR . $entity->getProfileImage() );
#dd($args->getObjectManager()->getMetadataFactory());
// Latest Activity
$criteria = Criteria::create()->where(Criteria::expr()->eq('exit_at', null));
// $criteria->orderBy(['created_at' => Criteria::DESC])->setMaxResults(1);
$criteria->orderBy(['id' => Criteria::DESC])->setMaxResults(1);
$latestActivity = $entity->getWorkerActivities()->matching($criteria)->first();
$latestActivity = $latestActivity instanceof WorkerActivities ? $latestActivity : null;
// Status
$status = false;
if ($latestActivity !== null) {
$today = new \DateTimeImmutable('now');
$exitAt = $latestActivity->getExitAt();
if ($exitAt) {
// Eğer çıkış zamanı gelecekteyse aktif kabul et
$status = $today < $exitAt;
} else {
// Çıkış zamanı yoksa hâlâ aktif
$status = true;
}
$latestActivity->setActivityStatus($status);
if($exitAt){
dd($exitAt, $today->diff($exitAt)->days, $status);
}
// Hesapladığın durumu activity'e ata
}
$entity
->setFullName( $entity->getName() . ' ' . $entity->getSurname() )
->setLatestActivity($latestActivity);
}
if ( $entity instanceof Partner ) {
$entity->setLogoImageFullPath( $this->serviceArgs['dirs']['partnerImagesFetchDir'] . DIRECTORY_SEPARATOR . $entity->getCompanyLogo() );
$entity->setCompanyLogo( $this->serviceArgs['dirs']['partnerImagesFetchDir'] . DIRECTORY_SEPARATOR . $entity->getCompanyLogo() );
}
if ( $entity instanceof Vendor ) {
$entity->setVendorLogo( $this->serviceArgs['dirs']['vendorLogosDirectory'] . DIRECTORY_SEPARATOR . $entity->getVendorLogo() );
}
if( $entity instanceof ProjectOrderTaskFulfillmentWorkDocs ){
dump($this->serviceArgs['dirs']['taskFulfillmentWorkDocsReadDir'] . DIRECTORY_SEPARATOR . $entity->getName());
$entity->setFile($this->serviceArgs['dirs']['taskFulfillmentWorkDocsReadDir'] . DIRECTORY_SEPARATOR . $entity->getName() );
}
if ( $entity instanceof WorkerInProject || $entity instanceof ProjectStakeholders || $entity instanceof Projects ) {
# 1,completed,success
# 2,in process,warning
$completed = false;
$processId = 2;
$message = $this->translator->trans('in process');
$projectName = '';
$alerted = ''; // Bitis zamani belli henuz zamani gelmemis
if( $entity instanceof Projects ){
// Core First of all
$project = $entity;
$projectEnded = $project->getEndAt() && ($project->getEndAt() < new \DateTimeImmutable('now'));
$projectName = $project->getName();
// $alerted = $project->getEndAt() && ($project->getEndAt() > new \DateTimeImmutable('now'));
$alerted = !is_null($project->getEndAt()) ? (new \DateTimeImmutable('now'))->diff($project->getEndAt())->days : 'Infinity';
if( $projectEnded ){
$params = "completed";
$completed = true;
}
}
elseif( $entity instanceof ProjectStakeholders ){
// Core First of all
$project = $entity->getProject();
$projectEnded = $project->getEndAt() && ($project->getEndAt() < new \DateTimeImmutable('now'));
$projectName = $project->getName();
// $alerted = !is_null($project->getEndAt()) && ($project->getEndAt() > new \DateTimeImmutable('now'));
$alerted = !is_null($project->getEndAt()) ? (new \DateTimeImmutable('now'))->diff($project->getEndAt())->days : 'Infinity';
if($projectEnded){
$completed = true;
$message = $this->translator->trans('completed');
$processId = 1;
} else {
// 2. Stakeholder
$project = $entity;
$projectEnded = $project->getEndAt() && ($project->getEndAt() < new \DateTimeImmutable('now'));
// $alerted = !is_null($project->getEndAt()) && ($project->getEndAt() > new \DateTimeImmutable('now'));
$alerted = !is_null($project->getEndAt()) ? (new \DateTimeImmutable('now'))->diff($project->getEndAt())->days : 'Infinity';
if($projectEnded){
$completed = true;
$message = $this->translator->trans('completed');
$processId = 1;
}
}
}
elseif( $entity instanceof WorkerInProject ){
// Core First of all
$project = $entity->getProject()->getProject();
$alerted = 'Infinity';
$projectEnded = false;
if(!is_null($project->getEndAt())){
$diff = (new \DateTimeImmutable('now'))->diff($project->getEndAt());
$days = $diff->days;
$invert = $diff->invert;
$projectEnded = $project->getEndAt() < new \DateTimeImmutable('now');
$alerted = $invert ? "-$days" : $days;
}
$projectName = $project->getName();
if($projectEnded){
$completed = true;
$message = $this->translator->trans('completed');
$processId = 1;
} else {
// 2. Stakeholder
$project = $entity->getProject();
$alerted = 'Infinity';
$projectEnded = false;
if(!is_null($project->getEndAt())){
$diff = (new \DateTimeImmutable('now'))->diff($project->getEndAt());
$days = $diff->days;
$invert = $diff->invert;
$projectEnded = $project->getEndAt() < new \DateTimeImmutable('now');
$alerted = $invert ? "-$days" : $days;
}
$projectName = $project->getProject()->getName();
if($projectEnded){
$completed = true;
$message = $this->translator->trans('completed');
$processId = 1;
} else {
$project = $entity;
$alerted = 'Infinity';
$projectEnded = false;
if(!is_null($project->getEndAt())){
$diff = (new \DateTimeImmutable('now'))->diff($project->getEndAt());
$days = $diff->days;
$invert = $diff->invert;
$projectEnded = $project->getEndAt() < new \DateTimeImmutable('now');
$alerted = $invert ? "-$days" : $days;
}
$projectName = $project->getProject()->getProject()->getName();
if($projectEnded){
$completed = true;
$message = $this->translator->trans('completed');
$processId = 1;
}
}
}
// TODO bu kisim gecici worker_in_project deki project prop u stakeholder_project prop u ile degisecek
$entity->setStakeholderProject($entity->getProject());
}
try{
#dump($message);
/**@var $found ProjectStatus*/
$found = $args->getObjectManager()->getRepository(ProjectStatus::class)->find($processId); // ->findOneBy(["name"=>$message]);
if(!is_null($found)){
$entity->setStatus([
"project_name" => $projectName,
'name' => $message,
'color' => $found->getColor(),
'completed' => $completed,
'alerted' => $alerted,
'show_and_notify' => $alerted !== 'Infinity' && $alerted > 0 && $alerted < 10,
'notify' => is_numeric($alerted) && $alerted > 0 ? $alerted . ' ' . $this->translator->trans('day(s) to end') : 'Infinity'
]);
}
else {
dd("Null by ProjectStatus [$message]");
}
} catch (\Exception $exception ){
throw new \Error("Error by ProjectStatus [$message]");
}
}
/**
* By Manage give an error
* @deprecated */
if( $entity instanceof ProjectOrders ){
return;
#dd($args->getObjectManager());
$entityManager = $args->getObjectManager();
$unitOfWork = $entityManager->getUnitOfWork();
$entityState = $unitOfWork->getEntityState($entity);
if($entityState === UnitOfWork::STATE_NEW) {
// return;
}
#return;
try{
if( is_null($entity->getInvoiceType())){
#dd(12);
$projectOwner = $entity->getProject()->getOwner();
// Set Standard
if(!$projectOwner){
$invoiceTypes = $args->getObjectManager()->getRepository(InvoiceType::class)->findOneBy(["name"=>"Standard"]);
#dd($invoiceTypes);
$entity->setInvoiceType($invoiceTypes);
return;
}
// Owner Defined but invoice type not selected than Set Standard
if(is_null($projectOwner->getInvoiceType())){
$invoiceTypes = $args->getObjectManager()->getRepository(InvoiceType::class)->findOneBy(["name"=>"Standard"]);
#dd($invoiceTypes);
#$entity->setInvoiceType($invoiceTypes);
return;
}
// dd($projectOwner->getInvoiceType());
$entity->setInvoiceType($projectOwner->getInvoiceType());
}
} catch (\Exception $exception){
}
}
if( $entity instanceof Stocks ){
$em = $args->getObjectManager();
$availableStockQuantity = $this->finalCalculatedStockQuantityFromTransactions($entity->getStockTransactions());
$entity->setAvailableStockQuantity( $availableStockQuantity );
}
if( $entity instanceof StockItems ){
$em = $args->getObjectManager();
$finalQuantities = $entity->getStocks()->map(function (/**@var Stocks $stock*/$stock) {
return $this->finalCalculatedStockQuantityFromTransactions($stock->getStockTransactions());
});
$entity->setTotalAvailableQuantity(array_sum($finalQuantities->toArray()));
}
// TODO move this to static as Metric into Invoice Table
if($entity instanceof Invoices){
// $calculated = $approved = $paid = [];
//
// /**@var $invoicePosition InvoicePositions */
// foreach ($entity->getInvoicePositions() as $invoicePosition) {
// // This position absolute from Billing generated
// $price = $invoicePosition->getOrderBilling()->getPrice();
// $unit = $invoicePosition->getOrderBilling()->getQuantity();
// // Required
// $calculated[] = $price * $unit;
// // Optional
// if(!is_null($entity->getApprovedAt())) {
// $approved[] = $entity->getApprovedAmount();
// }
// // Optional
// if(!is_null($entity->getPaidAt())){
// $paid[] = $invoicePosition->getPaidAmount();
// }
// }
//
// // After each
// $calculatedTotals = array_sum($calculated);
// $paidTotals = array_sum($paid);
// $approvedTotals = array_sum($approved);
// $paidStatus = $this->paymentStatusEnum::PAYMENT_STATUS_UNPAID;
// if($paidTotals > 0){
// // TODO Paid
// if( $paidTotals !== $calculatedTotals ){
// // TODO Partially Paid
// $paidStatus = $this->paymentStatusEnum::PAYMENT_STATUS_PARTIALLY_PAID;
// } else {
// // TODO Fully Paid
// $paidStatus = $this->paymentStatusEnum::PAYMENT_STATUS_PAID;
// }
// }
//
// $entity->setPaidStatus([
// "calculated" => $calculatedTotals, "paid" => $paidTotals, "status" => $paidStatus
// ]);
}
// Update Virtual Column
if($entity instanceof Invoices){
if(!is_null($entity->getPaidAt())){
$entity->setWeekOfYear($entity->getPaidAt()->format('o-W'));
}
}
}
private function finalCalculatedStockQuantityFromTransactions(PersistentCollection $transactions): float {
$finalQuantity = 0;
foreach ( $transactions as $transaction) {
$qty = $transaction->getQuantity();
if ($transaction->getTransactionType() === 'in') {
$finalQuantity += $qty;
} elseif ($transaction->getTransactionType() === 'out') {
$finalQuantity -= $qty;
}
}
return $finalQuantity;
}
private function setWorkerImagePathRuntime(Worker $worker): void
{
$profileImage = $worker->getProfileImage();
if (!$profileImage) {
return;
}
// 1️⃣ Eğer zaten path eklenmişse tekrar ekleme
$fetchDir = rtrim($this->serviceArgs['dirs']['workerImagesFetchDir'], DIRECTORY_SEPARATOR);
if (!str_starts_with($profileImage, $fetchDir)) {
// 2️⃣ Runtime olarak path’i set et
// Reflection ile Doctrine dirty check’i bypass et
$reflection = new \ReflectionProperty(Worker::class, 'profile_image');
$reflection->setAccessible(true);
$reflection->setValue($worker, $fetchDir . DIRECTORY_SEPARATOR . $profileImage);
// $reflectionFull = new \ReflectionProperty(Worker::class, 'profileImageFullPath');
// $reflectionFull->setAccessible(true);
// $reflectionFull->setValue($worker, $fetchDir . DIRECTORY_SEPARATOR . $profileImage);
}
}
}