commerce_shipping-8.x-2.0-rc2/src/EventSubscriber/ShipmentSubscriber.php
src/EventSubscriber/ShipmentSubscriber.php
<?php
namespace Drupal\commerce_shipping\EventSubscriber;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\commerce_order\Adjustment;
use Drupal\commerce_shipping\Event\ShipmentEvent;
use Drupal\commerce_shipping\Event\ShippingEvents;
use Drupal\commerce_shipping\Mail\ShipmentConfirmationMailInterface;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ShipmentSubscriber implements EventSubscriberInterface {
/**
* A static cache of shipments to clear on destruct(), keyed by order ID.
*
* @var array
*/
protected $shipmentsToClear;
/**
* Constructs a new ShipmentSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\commerce_shipping\Mail\ShipmentConfirmationMailInterface $shipmentConfirmationMail
* The shipment confirmation mail service.
*/
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected ShipmentConfirmationMailInterface $shipmentConfirmationMail,
) {
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
'commerce_shipment.ship.post_transition' => ['onShip'],
ShippingEvents::SHIPMENT_DELETE => ['onShipmentDelete'],
];
}
/**
* Checks shipment mail settings and sends confirmation email to customer.
*
* @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
* The transition event.
*/
public function onShip(WorkflowTransitionEvent $event): void {
/** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
$shipment = $event->getEntity();
/** @var \Drupal\commerce_shipping\Entity\ShipmentTypeInterface $shipment_type */
$shipment_type = $this->entityTypeManager->getStorage('commerce_shipment_type')->load($shipment->bundle());
$order = $shipment->getOrder();
assert($order !== NULL);
// Continue only if settings are configured to send confirmation.
if ($shipment_type->shouldSendConfirmation()) {
$this->shipmentConfirmationMail->send($shipment, $order->getEmail(), $shipment_type->getConfirmationBcc());
}
}
/**
* Reacts to a shipment being deleted.
*
* When a shipment gets deleted, ensure the order no longer references it
* on destruct(), and make sure the adjustments added by this shipment are
* removed. The reason why we're queueing this is to ensure we don't
* save the order during an order refresh which potentially causes data loss.
*
* @param \Drupal\commerce_shipping\Event\ShipmentEvent $event
* The shipment event.
*/
public function onShipmentDelete(ShipmentEvent $event): void {
$shipment = $event->getShipment();
if (!$shipment->getOrderId()) {
return;
}
// Shipment adjustments are transferred "unlocked" to the order
// (See LateOrderProcessor), we store them in static cache so we can
// properly remove them on destruct() if they still exist on the order.
$adjustments = array_map(function (Adjustment $adjustment) {
if ($adjustment->isLocked()) {
$adjustment = new Adjustment([
'locked' => FALSE,
] + $adjustment->toArray());
}
return $adjustment;
}, $shipment->getAdjustments());
// Because we wouldn't be able to load the shipment that is about to be
// deleted on destruct, store the adjustments that must be removed as well
// as the shipment ID.
// See ::clearShipments().
$this->shipmentsToClear[$shipment->getOrderId()][] = [
'adjustments' => (array) $adjustments,
'id' => $shipment->id(),
];
}
/**
* {@inheritdoc}
*/
public function destruct() {
if ($this->shipmentsToClear) {
$this->clearShipments();
}
}
/**
* Clears the shipments that were deleted during this request.
*
* This will ensure the order no longer references the deleted shipments and
* take care of removing any leftover adjustments.
*/
protected function clearShipments(): void {
/** @var \Drupal\commerce_order\Entity\OrderInterface[] $orders */
$orders = $this->entityTypeManager->getStorage('commerce_order')->loadMultiple(array_keys($this->shipmentsToClear));
foreach ($orders as $order) {
// Skip orders that no longer references shipments.
if (!$order->hasField('shipments') ||
$order->get('shipments')->isEmpty()) {
continue;
}
$order_shipments = $order->get('shipments');
$save_order = FALSE;
foreach ($this->shipmentsToClear[$order->id()] as $shipment_to_clear) {
$shipment_id = $shipment_to_clear['id'];
// Make sure the shipment is not being referenced by the order anymore.
/** @var \Drupal\Core\Field\FieldItemListInterface $order_shipments */
foreach ($order_shipments as $delta => $item) {
if ($item->target_id != $shipment_id) {
continue;
}
$save_order = TRUE;
// The shipment is still referenced by the order, clear the reference.
$order_shipments->removeItem($delta);
/** @var \Drupal\commerce_order\Adjustment[] $shipment_adjustments */
$shipment_adjustments = $shipment_to_clear['adjustments'];
$adjustments = [];
// Loop over the order adjustments, to remove any adjustment added
// by the shipment that was removed during the request.
foreach ($order->getAdjustments() as $adjustment) {
// Ensure the shipping adjustment is skipped/i.e not re-added
// if present.
if ($adjustment->getSourceId() == $shipment_id) {
continue;
}
$matching_adjustment = FALSE;
// Loop over the shipment adjustments, to see if the order has a
// matching adjustment that needs to be removed.
foreach ($shipment_adjustments as $shipment_adjustment) {
if ($shipment_adjustment->toArray() != $adjustment->toArray()) {
continue;
}
// A matching adjustment was found on the order, no need to
// continue the loop.
$matching_adjustment = TRUE;
break;
}
// A matching adjustment was found on the order, it should be
// removed, skipping it will ensure it's not re-added to the order.
if ($matching_adjustment) {
continue;
}
$adjustments[] = $adjustment;
}
$order->setAdjustments($adjustments);
}
}
if ($save_order) {
$order->save();
}
}
}
}
