<?php

namespace Ekyna\Component\Commerce\Common\Updater;

use Ekyna\Component\Commerce\Common\Builder\AddressBuilderInterface;
use Ekyna\Component\Commerce\Common\Builder\AdjustmentBuilderInterface;
use Ekyna\Component\Commerce\Common\Calculator\AmountCalculatorInterface;
use Ekyna\Component\Commerce\Common\Calculator\WeightCalculatorInterface;
use Ekyna\Component\Commerce\Common\Factory\SaleFactoryInterface;
use Ekyna\Component\Commerce\Common\Model\AddressInterface;
use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Common\Util\Money;
use Ekyna\Component\Commerce\Invoice\Calculator\InvoiceCalculatorInterface;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceStates;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceSubjectInterface;
use Ekyna\Component\Commerce\Payment\Calculator\PaymentCalculatorInterface;
use Ekyna\Component\Commerce\Payment\Model\PaymentStates;
use Ekyna\Component\Commerce\Payment\Model\PaymentTermTriggers;
use Ekyna\Component\Commerce\Payment\Releaser\ReleaserInterface;
use Ekyna\Component\Commerce\Shipment\Gateway\InStore\InStorePlatform;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentStates;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentSubjectInterface;
use Ekyna\Component\Commerce\Shipment\Resolver\ShipmentPriceResolverInterface;

/**
 * Class SaleUpdater
 * @package Ekyna\Component\Commerce\Common\Updater
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SaleUpdater implements SaleUpdaterInterface
{
    /**
     * @var AddressBuilderInterface
     */
    protected $addressBuilder;

    /**
     * @var AdjustmentBuilderInterface
     */
    protected $adjustmentBuilder;

    /**
     * @var AmountCalculatorInterface
     */
    protected $amountCalculator;

    /**
     * @var WeightCalculatorInterface
     */
    protected $weightCalculator;

    /**
     * @var ShipmentPriceResolverInterface
     */
    protected $shipmentPriceResolver;

    /**
     * @var PaymentCalculatorInterface
     */
    protected $paymentCalculator;

    /**
     * @var InvoiceCalculatorInterface
     */
    protected $invoiceCalculator;

    /**
     * @var ReleaserInterface
     */
    protected $outstandingReleaser;

    /**
     * @var SaleFactoryInterface
     */
    protected $saleFactory;

    /**
     * Constructor.
     *
     * @param AddressBuilderInterface        $addressBuilder
     * @param AdjustmentBuilderInterface     $adjustmentBuilder
     * @param AmountCalculatorInterface      $amountCalculator
     * @param WeightCalculatorInterface      $weightCalculator
     * @param ShipmentPriceResolverInterface $shipmentPriceResolver
     * @param PaymentCalculatorInterface     $paymentCalculator
     * @param InvoiceCalculatorInterface     $invoiceCalculator
     * @param ReleaserInterface              $outstandingReleaser
     * @param SaleFactoryInterface           $saleFactory
     */
    public function __construct(
        AddressBuilderInterface $addressBuilder,
        AdjustmentBuilderInterface $adjustmentBuilder,
        AmountCalculatorInterface $amountCalculator,
        WeightCalculatorInterface $weightCalculator,
        ShipmentPriceResolverInterface $shipmentPriceResolver,
        PaymentCalculatorInterface $paymentCalculator,
        InvoiceCalculatorInterface $invoiceCalculator,
        ReleaserInterface $outstandingReleaser,
        SaleFactoryInterface $saleFactory
    ) {
        $this->addressBuilder = $addressBuilder;
        $this->adjustmentBuilder = $adjustmentBuilder;
        $this->amountCalculator = $amountCalculator;
        $this->weightCalculator = $weightCalculator;
        $this->shipmentPriceResolver = $shipmentPriceResolver;
        $this->paymentCalculator = $paymentCalculator;
        $this->invoiceCalculator = $invoiceCalculator;
        $this->outstandingReleaser = $outstandingReleaser;
        $this->saleFactory = $saleFactory;
    }

    /**
     * @inheritdoc
     */
    public function recalculate(SaleInterface $sale, $force = false)
    {
        $changed = false;

        // TODO Test
        // 1. discounts
        if (0 == $sale->getPaidTotal()) { // Do not update if paid amount
            $changed = $this->updateDiscounts($sale);
        }
        // 2. weight total
        $changed |= $this->updateWeightTotal($sale);
        // 3. shipment amount (based on 2.)
        $changed |= $this->updateShipmentMethodAndAmount($sale);
        // 4. taxation (items and shipment)
        $changed |= $this->updateTaxation($sale);
        // 5. net and grand totals
        $changed |= $this->updateAmountsTotal($sale);
        // 6. paid and outstanding totals
        $changed |= $this->updatePaymentTotal($sale);
        // 7. invoice total
        $changed |= $this->updateInvoiceTotal($sale);

        return $changed;
    }

    /**
     * @inheritdoc
     */
    public function updateTotals(SaleInterface $sale)
    {
        $changed = $this->updateWeightTotal($sale);

        $changed |= $this->updateAmountsTotal($sale);

        $changed |= $this->updatePaymentTotal($sale);

        $changed |= $this->updateInvoiceTotal($sale);

        return $changed;
    }

    /**
     * @inheritdoc
     */
    public function updateInvoiceAddressFromAddress(
        SaleInterface $sale,
        AddressInterface $source,
        $persistence = false
    ) {
        return $this->addressBuilder->buildSaleInvoiceAddressFromAddress($sale, $source, $persistence);
    }

    /**
     * @inheritdoc
     */
    public function updateDeliveryAddressFromAddress(
        SaleInterface $sale,
        AddressInterface $source,
        $persistence = false
    ) {
        return $this->addressBuilder->buildSaleDeliveryAddressFromAddress($sale, $source, $persistence);
    }

    /**
     * @inheritdoc
     */
    public function updateDiscounts(SaleInterface $sale, $persistence = false)
    {
        $changed = $this->adjustmentBuilder->buildDiscountAdjustmentsForSaleItems($sale, $persistence);

        return $this->adjustmentBuilder->buildDiscountAdjustmentsForSale($sale, $persistence) || $changed;
    }

    /**
     * @inheritdoc
     */
    public function updateTaxation(SaleInterface $sale, $persistence = false)
    {
        $changed = $this->adjustmentBuilder->buildTaxationAdjustmentsForSaleItems($sale, $persistence);

        return $this->adjustmentBuilder->buildTaxationAdjustmentsForSale($sale, $persistence) || $changed;
    }

    /**
     * @inheritdoc
     */
    public function updateShipmentTaxation(SaleInterface $sale, $persistence = false)
    {
        return $this->adjustmentBuilder->buildTaxationAdjustmentsForSale($sale, $persistence);
    }

    /**
     * @inheritdoc
     */
    public function updateShipmentMethodAndAmount(SaleInterface $sale)
    {
        // Abort if sale auto shipping is disabled
        if (!$sale->isAutoShipping()) {
            return false;
        }

        // Abort if sale has payment with non deletable state.
        foreach ($sale->getPayments() as $payment) {
            if (!PaymentStates::isDeletableState($payment->getState())) {
                return false;
            }
        }

        $updated = false;
        $prices = $this->shipmentPriceResolver->getAvailablePricesBySale($sale);

        // Assert that the sale's shipment method is still available
        if (null !== $initialMethod = $sale->getShipmentMethod()) {
            $found = false;
            foreach ($prices as $price) {
                if ($price->getMethod() === $initialMethod) {
                    $found = true;
                    break;
                }
            }
            if (!$found) {
                $sale->setShipmentMethod(null);
                $updated = true;
            }
        }

        // If sale does not have a shipment method, set the cheaper one
        if (null === $method = $sale->getShipmentMethod()) {
            $price = false;
            if (!empty($prices)) {
                // Find the cheapest non 'in store' shipment method
                foreach ($prices as $price) {
                    if ($price->getMethod()->getPlatformName() !== InStorePlatform::NAME) {
                        break;
                    }
                }
                // Not found, pick the first one
                if (false === $price) {
                    $price = reset($prices);
                }
            }
            if ($price) {
                $sale->setShipmentMethod($method = $price->getMethod());
                $updated = true;
            } else {
                // Revert to initial method
                $sale->setShipmentMethod($method = $initialMethod);
                $updated = false;
            }
        }

        // Resolve shipping cost
        $amount = 0;
        if (null !== $method) {
            if (null !== $price = $this->shipmentPriceResolver->getPriceBySale($sale)) {
                $amount = $price->isFree() ? 0 : $price->getPrice();
            }
        }

        // Update sale's shipping cost if needed
        if ($amount != $sale->getShipmentAmount()) {
            $sale->setShipmentAmount($amount);
            $updated = true;
        }

        return $updated;
    }

    /**
     * @inheritdoc
     */
    public function updatePaymentTerm(SaleInterface $sale)
    {
        // Don't override payment term if set
        if (null !== $sale->getPaymentTerm()) {
            return false;
        }

        $term = null;
        if (null !== $customer = $sale->getCustomer()) {
            // From parent if available
            if ($customer->hasParent()) {
                $term = $customer->getParent()->getPaymentTerm();
            } else {
                $term = $customer->getPaymentTerm();
            }
        }

        if ($term !== $sale->getPaymentTerm()) {
            $sale->setPaymentTerm($term);

            return true;
        }

        return false;
    }

    /**
     * @inheritdoc
     */
    public function updateOutstandingDate(SaleInterface $sale)
    {
        $date = $this->resolveOutstandingDate($sale);

        if ($date !== $sale->getOutstandingDate()) {
            $sale->setOutstandingDate($date);

            return true;
        }

        return false;
    }

    /**
     * Updates the total weight.
     *
     * @param SaleInterface $sale
     *
     * @return bool Whether the sale has been changed or not.
     */
    protected function updateWeightTotal(SaleInterface $sale)
    {
        $weightTotal = $this->weightCalculator->calculateSale($sale);

        if ($sale->getWeightTotal() != $weightTotal) {
            $sale->setWeightTotal($weightTotal);

            return true;
        }

        return false;
    }

    /**
     * Updates the sale's net and grand totals.
     *
     * @param SaleInterface $sale
     *
     * @return bool Whether the sale has been changed or not.
     */
    protected function updateAmountsTotal(SaleInterface $sale)
    {
        $changed = false;

        $currency = $sale->getCurrency()->getCode();

        $sale->clearResults();

        $result = $this->amountCalculator->calculateSale($sale);

        if (0 != Money::compare($result->getBase(), $sale->getNetTotal(), $currency)) {
            $sale->setNetTotal($result->getBase());
            $changed = true;
        }

        if (0 != Money::compare($result->getTotal(), $sale->getGrandTotal(), $currency)) {
            $sale->setGrandTotal($result->getTotal());
            $changed = true;
        }

        return $changed;
    }

    /**
     * Updates the payment totals total.
     *
     * @param SaleInterface $sale
     *
     * @return bool Whether the sale has been changed or not.
     */
    protected function updatePaymentTotal(SaleInterface $sale)
    {
        $changed = false;

        $currency = $sale->getCurrency()->getCode();

        // Update paid total if needed
        $paid = $this->paymentCalculator->calculatePaidTotal($sale);
        if (0 != Money::compare($paid, $sale->getPaidTotal(), $currency)) {
            $sale->setPaidTotal($paid);
            $changed = true;
        }
        // Update pending total total if needed
        $pending = $this->paymentCalculator->calculateOfflinePendingTotal($sale);
        if (0 != Money::compare($pending, $sale->getPendingTotal(), $currency)) {
            $sale->setPendingTotal($pending);
            $changed = true;
        }
        // Update accepted outstanding total if needed
        $acceptedOutstanding = $this->paymentCalculator->calculateOutstandingAcceptedTotal($sale);
        if (0 != Money::compare($acceptedOutstanding, $sale->getOutstandingAccepted(), $currency)) {
            $sale->setOutstandingAccepted($acceptedOutstanding);
            $changed = true;
        }
        // Update expired outstanding total if needed
        $expiredOutstanding = $this->paymentCalculator->calculateOutstandingExpiredTotal($sale);
        if (0 != Money::compare($expiredOutstanding, $sale->getOutstandingExpired(), $currency)) {
            $sale->setOutstandingExpired($expiredOutstanding);
            $changed = true;
        }

        // If payment totals has changed and fund has been released
        if ($changed && $this->outstandingReleaser->releaseFund($sale)) {
            // Re-update the outstanding totals
            //$sale->setPaidTotal($this->paymentCalculator->calculatePaidTotal($sale));
            //$sale->setPendingTotal($this->paymentCalculator->calculateOfflinePendingTotal($sale));
            $sale->setOutstandingAccepted($this->paymentCalculator->calculateOutstandingAcceptedTotal($sale));
            $sale->setOutstandingExpired($this->paymentCalculator->calculateOutstandingExpiredTotal($sale));
        }

        return $changed;
    }

    /**
     * Updates the sale invoice total.
     *
     * @param SaleInterface $sale
     *
     * @return bool Whether the sale has been changed or not.
     */
    protected function updateInvoiceTotal(SaleInterface $sale)
    {
        if (!$sale instanceof InvoiceSubjectInterface) {
            return false;
        }

        $changed = false;

        $invoice = $this->invoiceCalculator->calculateInvoiceTotal($sale);
        if (0 != Money::compare($invoice, $sale->getInvoiceTotal(), $sale->getCurrency()->getCode())) {
            $sale->setInvoiceTotal($invoice);

            $changed = true;
        }

        $credit = $this->invoiceCalculator->calculateCreditTotal($sale);
        if (0 != Money::compare($credit, $sale->getCreditTotal(), $sale->getCurrency()->getCode())) {
            $sale->setCreditTotal($credit);

            $changed = true;
        }

        return $changed;
    }

    /**
     * Resolves the outstanding date.
     *
     * @param SaleInterface $sale
     *
     * @return \DateTime|null
     */
    protected function resolveOutstandingDate(SaleInterface $sale)
    {
        if (!$sale instanceof InvoiceSubjectInterface) {
            return null;
        }
        if (!$sale instanceof ShipmentSubjectInterface) {
            return null;
        }

        if (null === $term = $sale->getPaymentTerm()) {
            return null;
        }

        if (!$this->saleHasOutstandingPayments($sale)) {
            return null;
        }

        $from = null;
        switch ($term->getTrigger()) {
            case PaymentTermTriggers::TRIGGER_SHIPPED:
                $from = $sale->getShippedAt();
                break;

            case PaymentTermTriggers::TRIGGER_FULLY_SHIPPED:
                if ($sale->getShipmentState() === ShipmentStates::STATE_COMPLETED) {
                    $from = $sale->getShippedAt(true);
                }
                break;

            case PaymentTermTriggers::TRIGGER_INVOICED:
                $from = $sale->getInvoicedAt();
                break;

            case PaymentTermTriggers::TRIGGER_FULLY_INVOICED:
                if ($sale->getInvoiceState() === InvoiceStates::STATE_COMPLETED) {
                    $from = $sale->getInvoicedAt(true);
                }
                break;
        }

        if (null === $from) {
            return null;
        }

        // Calculate outstanding date
        $date = clone $from;
        $date->setTime(23, 59, 59);
        $date->modify(sprintf('+%s days', $term->getDays()));
        if ($term->getEndOfMonth()) {
            $date->modify('last day of this month');
        }

        return $date;
    }

    /**
     * Returns whether the sale has (accepted/expired) outstanding payments.
     *
     * @param SaleInterface $sale
     *
     * @return bool
     */
    protected function saleHasOutstandingPayments(SaleInterface $sale)
    {
        $allowedStates = [
            PaymentStates::STATE_CAPTURED,
            PaymentStates::STATE_AUTHORIZED,
            PaymentStates::STATE_EXPIRED,
        ];
        foreach ($sale->getPayments() as $payment) {
            if ($payment->getMethod()->isOutstanding() && in_array($payment->getState(), $allowedStates, true)) {
                return true;
            }
        }

        return false;
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints;

use Ekyna\Component\Commerce\Common\Model\SaleItemInterface;
use Ekyna\Component\Commerce\Exception\ValidationFailedException;
use Ekyna\Component\Commerce\Invoice\Calculator\InvoiceCalculatorInterface;
use Ekyna\Component\Commerce\Invoice\Model as Invoice;
use Ekyna\Component\Commerce\Shipment\Calculator\ShipmentCalculatorInterface;
use Ekyna\Component\Commerce\Shipment\Model as Shipment;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class SaleItemValidator
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SaleItemValidator extends ConstraintValidator
{
    /**
     * @var InvoiceCalculatorInterface
     */
    protected $invoiceCalculator;

    /**
     * @var ShipmentCalculatorInterface
     */
    protected $shipmentCalculator;

    /**
     * Constructor.
     *
     * @param InvoiceCalculatorInterface  $invoiceCalculator
     * @param ShipmentCalculatorInterface $shipmentCalculator
     */
    public function __construct(
        InvoiceCalculatorInterface $invoiceCalculator,
        ShipmentCalculatorInterface $shipmentCalculator
    ) {
        $this->invoiceCalculator = $invoiceCalculator;
        $this->shipmentCalculator = $shipmentCalculator;
    }

    /**
     * {@inheritdoc}
     */
    public function validate($item, Constraint $constraint)
    {
        if (null === $item) {
            return;
        }

        if (!$item instanceof SaleItemInterface) {
            throw new UnexpectedTypeException($item, SaleItemInterface::class);
        }
        if (!$constraint instanceof SaleItem) {
            throw new UnexpectedTypeException($constraint, SaleItem::class);
        }

        try {
            $this->checkPrivacyIntegrity($item, $constraint);
            $this->checkInvoiceIntegrity($item, $constraint);
            $this->checkShipmentIntegrity($item, $constraint);
        } catch (ValidationFailedException $e) {
            return;
        }
    }

    /**
     * Checks the sale item privacy integrity.
     *
     * @param SaleItemInterface $item
     * @param SaleItem          $constraint
     *
     * @throws ValidationFailedException
     */
    protected function checkPrivacyIntegrity(SaleItemInterface $item, SaleItem $constraint)
    {
        $parent = $item->getParent();

        if ($item->isPrivate()) {
            if (null === $parent) {
                // Level zero items must be public
                $this
                    ->context
                    ->buildViolation($constraint->root_item_cant_be_private)
                    ->atPath('private')
                    ->addViolation();

                throw new ValidationFailedException();
            } elseif ($item->getTaxGroup() !== $parent->getTaxGroup()) {
                // Tax group must match parent's one
                $this
                    ->context
                    ->buildViolation($constraint->tax_group_integrity)
                    ->atPath('taxGroup')
                    ->addViolation();

                throw new ValidationFailedException();
            }
        } elseif (null !== $parent && $parent->isPrivate()) {
            // Item with private parent must be private
            $this
                ->context
                ->buildViolation($constraint->privacy_integrity)
                ->atPath('private')
                ->addViolation();

            throw new ValidationFailedException();
        }
    }

    /**
     * Checks that the sale item quantity is greater than or equals the invoiced quantity.
     *
     * @param SaleItemInterface $item
     * @param SaleItem          $constraint
     *
     * @throws ValidationFailedException
     */
    protected function checkInvoiceIntegrity(SaleItemInterface $item, SaleItem $constraint)
    {
        $sale = $item->getSale();
        if (!$sale instanceof Invoice\InvoiceSubjectInterface) {
            return;
        }

        if (empty($sale->getInvoices()->toArray())) {
            return;
        }

        $min = $this->invoiceCalculator->calculateInvoicedQuantity($item);
        $qty = $item->getTotalQuantity();

        // TODO Use packaging format
        if (1 === bccomp($min, 0, 3) && 1 === bccomp($min, $qty, 3)) {
            $this
                ->context
                ->buildViolation($constraint->quantity_is_lower_than_invoiced, [
                    '%min%' => $min,
                ])
                ->atPath('quantity')
                ->addViolation();

            throw new ValidationFailedException();
        }
    }

    /**
     * Checks that the sale item quantity is greater than or equals the shipped quantity.
     *
     * @param SaleItemInterface $item
     * @param SaleItem          $constraint
     *
     * @throws ValidationFailedException
     */
    protected function checkShipmentIntegrity(SaleItemInterface $item, SaleItem $constraint)
    {
        $sale = $item->getSale();
        if (!$sale instanceof Shipment\ShipmentSubjectInterface) {
            return;
        }

        if (empty($sale->getShipments()->toArray())) {
            return;
        }

        $min = $this->shipmentCalculator->calculateShippedQuantity($item);

        // TODO Use packaging format
        //if (0 < $min && $item->getTotalQuantity() < $min) {
        if (1 === bccomp($min, 0, 3) && 1 === bccomp($min, $item->getTotalQuantity(), 3)) {
            $this
                ->context
                ->buildViolation($constraint->quantity_is_lower_than_shipped, [
                    '%min%' => $min,
                ])
                ->setInvalidValue($item->getQuantity())
                ->atPath('quantity')
                ->addViolation();

            throw new ValidationFailedException();
        }
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints;

use Ekyna\Component\Commerce\Accounting\Model\AccountingInterface;
use Ekyna\Component\Commerce\Accounting\Model\AccountingTypes;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class AccountingValidator
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints
 * @author  Étienne Dauvergne <contact@ekyna.com>
 */
class AccountingValidator extends ConstraintValidator
{
    /**
     * {@inheritdoc}
     */
    public function validate($accounting, Constraint $constraint)
    {
        if (!$accounting instanceof AccountingInterface) {
            throw new UnexpectedTypeException($accounting, AccountingInterface::class);
        }
        if (!$constraint instanceof Accounting) {
            throw new UnexpectedTypeException($constraint, Accounting::class);
        }

        $tax = null;
        $taxRule = false;
        $paymentMethod = false;
        $customerGroups = false;

        // Requirements
        if ($accounting->getType() === AccountingTypes::TYPE_PAYMENT) {
            $paymentMethod = true;
            $tax = false;
        } elseif ($accounting->getType() === AccountingTypes::TYPE_UNPAID) {
            $customerGroups = null;
        } elseif ($accounting->getType() === AccountingTypes::TYPE_TAX) {
            $tax = true;
        } else {
            $taxRule = true;
        }

        // Tax assertion
        if (!$accounting->getTax() && $tax) {
            $this
                ->context
                ->buildViolation($constraint->tax_is_required)
                ->atPath('tax')
                ->addViolation();
        } elseif ($accounting->getTax() && false === $tax) {
            $this
                ->context
                ->buildViolation($constraint->tax_must_be_null)
                ->atPath('tax')
                ->addViolation();
        }

        // Tax rule assertion
        if (!$accounting->getTaxRule() && $taxRule) {
            $this
                ->context
                ->buildViolation($constraint->tax_rule_is_required)
                ->atPath('taxRule')
                ->addViolation();
        } elseif ($accounting->getTaxRule() && false === $taxRule) {
            $this
                ->context
                ->buildViolation($constraint->tax_rule_must_be_null)
                ->atPath('taxRule')
                ->addViolation();
        }

        // Payment method assertion
        if (!$accounting->getPaymentMethod() && $paymentMethod) {
            $this
                ->context
                ->buildViolation($constraint->payment_method_is_required)
                ->atPath('paymentMethod')
                ->addViolation();
        } elseif ($accounting->getPaymentMethod() && false === $paymentMethod) {
            $this
                ->context
                ->buildViolation($constraint->payment_method_must_be_null)
                ->atPath('paymentMethod')
                ->addViolation();
        }

        // Customer groups assertion
        if ((0 === $accounting->getCustomerGroups()->count()) && $customerGroups) {
            $this
                ->context
                ->buildViolation($constraint->customer_groups_is_required)
                ->atPath('customerGroups')
                ->addViolation();
        } elseif ((0 < $accounting->getCustomerGroups()->count()) && false === $customerGroups) {
            $this
                ->context
                ->buildViolation($constraint->customer_groups_must_be_empty)
                ->atPath('customerGroups')
                ->addViolation();
        }
    }
}


<?php

namespace Ekyna\Component\Commerce\Stock\Linker;

use Ekyna\Component\Commerce\Common\Currency\CurrencyConverterInterface;
use Ekyna\Component\Commerce\Common\Util\Money;
use Ekyna\Component\Commerce\Exception\StockLogicException;
use Ekyna\Component\Commerce\Stock\Model\StockUnitInterface;
use Ekyna\Component\Commerce\Stock\Resolver\StockUnitResolverInterface;
use Ekyna\Component\Commerce\Stock\Updater\StockUnitUpdaterInterface;
use Ekyna\Component\Commerce\Supplier\Model\SupplierOrderItemInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

/**
 * Class StockUnitLinker
 * @package Ekyna\Component\Commerce\Stock\Linker
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class StockUnitLinker implements StockUnitLinkerInterface
{
    /**
     * @var PersistenceHelperInterface
     */
    protected $persistenceHelper;

    /**
     * @var StockUnitUpdaterInterface
     */
    protected $stockUnitUpdater;

    /**
     * @var StockUnitResolverInterface
     */
    protected $unitResolver;

    /**
     * @var CurrencyConverterInterface
     */
    protected $currencyConverter;

    /**
     * Constructor.
     *
     * @param PersistenceHelperInterface $persistenceHelper
     * @param StockUnitUpdaterInterface  $stockUnitUpdater
     * @param StockUnitResolverInterface $unitResolver
     * @param CurrencyConverterInterface $currencyConverter
     */
    public function __construct(
        PersistenceHelperInterface $persistenceHelper,
        StockUnitUpdaterInterface $stockUnitUpdater,
        StockUnitResolverInterface $unitResolver,
        CurrencyConverterInterface $currencyConverter
    ) {
        $this->persistenceHelper = $persistenceHelper;
        $this->stockUnitUpdater = $stockUnitUpdater;
        $this->unitResolver = $unitResolver;
        $this->currencyConverter = $currencyConverter;
    }

    /**
     * @inheritdoc
     */
    public function linkItem(SupplierOrderItemInterface $supplierOrderItem)
    {
        if (!$supplierOrderItem->hasSubjectIdentity()) {
            return;
        }

        // Find 'unlinked' stock units ordered (+ Cached 'new' stock units look up)
        if (null !== $stockUnit = $this->unitResolver->findLinkable($supplierOrderItem)) {
            $stockUnit->setSupplierOrderItem($supplierOrderItem);
        } else {
            $stockUnit = $this->unitResolver->createBySubjectRelative($supplierOrderItem);
        }

        $stockUnit
            ->setSupplierOrderItem($supplierOrderItem)
            ->setEstimatedDateOfArrival($supplierOrderItem->getOrder()->getEstimatedDateOfArrival());

        $this->updatePrice($stockUnit);

        $this->stockUnitUpdater->updateOrdered($stockUnit, $supplierOrderItem->getQuantity(), false);
    }

    /**
     * @inheritdoc
     */
    public function applyItem(SupplierOrderItemInterface $supplierOrderItem)
    {
        if (!$supplierOrderItem->hasSubjectIdentity()) {
            return false;
        }

        // Supplier order item has been previously linked to a stock unit.
        $stockUnit = $supplierOrderItem->getStockUnit();

        $changed = false;

        // Update net price if needed
        if ($this->persistenceHelper->isChanged($supplierOrderItem, 'netPrice')) {
            if ($this->updatePrice($stockUnit)) {
                $this->persistenceHelper->persistAndRecompute($stockUnit, false);
                $changed = true;
            }
        }

        // Update ordered quantity if needed
        if ($this->persistenceHelper->isChanged($supplierOrderItem, 'quantity')) {
            $cs = $this->persistenceHelper->getChangeSet($supplierOrderItem, 'quantity');
            if (0 != $cs[1] - $cs[0]) { // TODO Use packaging format
                $this->stockUnitUpdater->updateOrdered($stockUnit, $supplierOrderItem->getQuantity(), false);
                $changed = true;
            }
        }

        return $changed;
    }

    /**
     * @inheritdoc
     */
    public function unlinkItem(SupplierOrderItemInterface $supplierOrderItem)
    {
        if (!$supplierOrderItem->hasSubjectIdentity()) {
            return;
        }

        if (null === $stockUnit = $supplierOrderItem->getStockUnit()) {
            return;
        }

        // Unlink stock unit by setting supplier order item to null and ordered quantity to zero
        $stockUnit
            ->setSupplierOrderItem(null)
            ->setNetPrice(null)
            ->setEstimatedDateOfArrival(null);

        $this->stockUnitUpdater->updateOrdered($stockUnit, 0, false);
    }

    /**
     * Updates the stock unit price.
     *
     * @param StockUnitInterface $stockUnit
     *
     * @return bool Whether or not the net price has been updated.
     *
     * @throws \Ekyna\Component\Commerce\Exception\LogicException
     */
    private function updatePrice(StockUnitInterface $stockUnit)
    {
        $price = null;

        if (null !== $item = $stockUnit->getSupplierOrderItem()) {
            if (null === $order = $item->getOrder()) {
                throw new StockLogicException("Supplier order item's order must be set at this point.");
            }

            $currency = $order->getCurrency()->getCode();
            $date = $order->getPaymentDate();
            if ($date > new \DateTime()) {
                $date = null;
            }

            $price = $this->currencyConverter->convert($item->getNetPrice(), $currency, null, $date);
        }

        if (0 !== Money::compare($stockUnit->getNetPrice(), $price, $this->currencyConverter->getDefaultCurrency())) {
            $stockUnit->setNetPrice($price);

            return true;
        }

        return false;
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\View;

/**
 * Class SaleView
 * @package Ekyna\Component\Commerce\Common\View
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SaleView extends AbstractView
{
    /**
     * @var string
     */
    private $template;

    /**
     * @var bool
     */
    private $ati;

    /**
     * @var TotalView
     */
    private $gross;

    /**
     * @var TotalView
     */
    private $final;

    /**
     * @var MarginView
     */
    private $margin;

    /**
     * @var LineView[]
     */
    private $items;

    /**
     * @var LineView[]
     */
    private $discounts;

    /**
     * @var LineView
     */
    private $shipment;

    /**
     * @var TaxView[]
     */
    private $taxes;

    /**
     * @var string[]
     */
    private $alerts;

    /**
     * @var string[]
     */
    private $messages;

    /**
     * @var array
     */
    private $translations;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->ati = true;
        $this->items = [];
        $this->discounts = [];
        $this->taxes = [];
        $this->messages = [];
        $this->alerts = [];

        $this->translations = $this->getDefaultTranslations();

        $this->vars = [
            'buttons'           => [],
            'show_availability' => false,
            'show_taxes'        => false,
            'show_discount'     => false,
            'show_margin'       => false,
        ];
    }

    /**
     * Sets the template.
     *
     * @param string $template
     *
     * @return SaleView
     */
    public function setTemplate(string $template)
    {
        $this->template = $template;

        return $this;
    }

    /**
     * Returns the template.
     *
     * @return string
     */
    public function getTemplate()
    {
        return $this->template;
    }

    /**
     * Sets the ati.
     *
     * @param bool $ati
     *
     * @return SaleView
     */
    public function setAti(bool $ati)
    {
        $this->ati = $ati;

        return $this;
    }

    /**
     * Returns the ati.
     *
     * @return bool
     */
    public function isAti()
    {
        return $this->ati;
    }

    /**
     * Sets the gross.
     *
     * @param TotalView $gross
     *
     * @return SaleView
     */
    public function setGross(TotalView $gross)
    {
        $this->gross = $gross;

        return $this;
    }

    /**
     * Returns the gross total view.
     *
     * @return TotalView
     */
    public function getGross()
    {
        return $this->gross;
    }

    /**
     * Sets the final.
     *
     * @param TotalView $final
     *
     * @return SaleView
     */
    public function setFinal(TotalView $final)
    {
        $this->final = $final;

        return $this;
    }

    /**
     * Returns the final total view.
     *
     * @return TotalView
     */
    public function getFinal()
    {
        return $this->final;
    }

    /**
     * Sets the margin view.
     *
     * @param MarginView $margin
     *
     * @return SaleView
     */
    public function setMargin($margin)
    {
        $this->margin = $margin;

        return $this;
    }

    /**
     * Returns the margin view.
     *
     * @return MarginView
     */
    public function getMargin()
    {
        return $this->margin;
    }

    /**
     * Adds the item line.
     *
     * @param LineView $line
     *
     * @return $this
     */
    public function addItem(LineView $line)
    {
        $this->items[] = $line;

        return $this;
    }

    /**
     * Returns the items lines views.
     *
     * @return LineView[]
     */
    public function getItems()
    {
        return $this->items;
    }

    /**
     * Adds the discount line.
     *
     * @param LineView $line
     *
     * @return $this
     */
    public function addDiscount(LineView $line)
    {
        $this->discounts[] = $line;

        return $this;
    }

    /**
     * Returns the discounts lines views.
     *
     * @return LineView[]
     */
    public function getDiscounts()
    {
        return $this->discounts;
    }

    /**
     * Sets the shipment line.
     *
     * @param LineView $line
     */
    public function setShipment(LineView $line)
    {
        $this->shipment = $line;
    }

    /**
     * Returns the shipment line view.
     *
     * @return LineView|null
     */
    public function getShipment()
    {
        return $this->shipment;
    }

    /**
     * Adds the tax view.
     *
     * @param TaxView $view
     *
     * @return $this
     */
    public function addTax(TaxView $view)
    {
        $this->taxes[] = $view;

        return $this;
    }

    /**
     * Returns the taxes views.
     *
     * @return TaxView[]
     */
    public function getTaxes()
    {
        return $this->taxes;
    }

    /**
     * Adds the button.
     *
     * @param Button $button
     */
    public function addButton(Button $button)
    {
        $this->vars['buttons'][] = $button;
    }

    /**
     * Adds the alert.
     *
     * @param string $alert
     *
     * @return SaleView
     */
    public function addAlert(string $alert)
    {
        $this->alerts[] = $alert;

        return $this;
    }

    /**
     * Returns the alerts.
     *
     * @return string[]
     */
    public function getAlerts()
    {
        return $this->alerts;
    }

    /**
     * Adds the message.
     *
     * @param string $message
     *
     * @return SaleView
     */
    public function addMessage(string $message)
    {
        $this->messages[] = $message;

        return $this;
    }

    /**
     * Returns the messages.
     *
     * @return string[]
     */
    public function getMessages()
    {
        return $this->messages;
    }

    /**
     * Returns the translations.
     *
     * @return array
     */
    public function getTranslations()
    {
        return $this->translations;
    }

    /**
     * Sets the translations.
     *
     * @param array $translations
     */
    public function setTranslations(array $translations)
    {
        foreach ($translations as $key => $string) {
            if (!(is_string($string) && !empty($string))) {
                throw new \InvalidArgumentException("Invalid translation for key '$key'.");
            }
        }

        $this->translations = array_replace($this->getDefaultTranslations(), $translations);
    }

    /**
     * Returns the default translations.
     *
     * @return array
     */
    public function getDefaultTranslations()
    {
        return [
            'designation'    => 'Designation',
            'reference'      => 'Reference',
            'availability'   => 'Avai.',
            'unit_net_price' => 'Unit Price',
            'unit_ati_price' => 'Unit Price',
            'quantity'       => 'Quantity',

            'net_gross'      => 'Gross',
            'ati_gross'      => 'Gross',
            'discount'       => 'Discount',

            'tax_rate'       => 'Tax rate',
            'tax_name'       => 'Tax',
            'tax_amount'     => 'Amount',

            'gross_totals'   => 'Gross totals',
            'net_total'      => 'Net total',
            'tax_total'      => 'Tax total',
            'grand_total'    => 'Grand total',
            'margin'         => 'Margin',
        ];
    }
}


<?php

/*
 * This file is part of the OrkestraApplicationBundle package.
 *
 * Copyright (c) Orkestra Community
 *
 * For the full copyright and license information, please view the LICENSE file
 * that was distributed with this source code.
 */

namespace Orkestra\Bundle\ApplicationBundle\Controller;

use JMS\SecurityExtraBundle\Annotation\Secure;
use Orkestra\Bundle\ApplicationBundle\Form\ChangePasswordType;
use Orkestra\Bundle\ApplicationBundle\Form\ProfileType;
use Orkestra\Bundle\ApplicationBundle\Http\JsonErrorResponse;
use Orkestra\Bundle\ApplicationBundle\Http\JsonReloadResponse;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;

/**
 * Profile controller.
 *
 * @Route("/profile")
 */
class ProfileController extends Controller
{
    /**
     * Shows the form  to edit the current user's profile
     *
     * @Route("/edit", name="orkestra_profile_edit")
     * @Secure(roles="ROLE_USER")
     * @Template()
     */
    public function editAction()
    {
        $user = $this->getUser();
        $form = $this->createForm(ProfileType::class, $user);

        return array(
            'form' => $form->createView(),
        );
    }

    /**
     * Updates the current user's profile
     *
     * @Route("/update", name="orkestra_profile_update", defaults={"_format"="json"})
     * @Secure(roles="ROLE_USER")
     * @Method("POST")
     */
    public function updateAction(Request $request)
    {
        $user = $this->getUser();
        $form = $this->createForm(ProfileType::class, $user);

        $form->bind($request);

        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();

            try {
                $em->persist($user);
                $em->flush();

                $this->get('session')->getFlashBag()->set('success', 'Your changes have been saved.');

                return new JsonReloadResponse();
            } catch (\Exception $e) {
                $form->addError(new FormError('Could not save changes. If the problem persists, please contact support.'));
            }
        }

        return new JsonErrorResponse($form);
    }

    /**
     * Shows a form to edit the current user's password
     *
     * @Route("/password/edit", name="orkestra_profile_password_edit")
     * @Secure(roles="ROLE_USER")
     * @Template()
     */
    public function editPasswordAction()
    {
        $form = $this->createForm(ChangePasswordType::class);

        return array(
            'form' => $form->createView(),
        );
    }

    /**
     * Updates the current user's password
     *
     * @Route("/password/update", name="orkestra_profile_password_update", defaults={"_format"="json"})
     * @Secure(roles="ROLE_USER")
     * @Method("POST")
     */
    public function updatePasswordAction(Request $request)
    {
        $user = $this->getUser();
        $form = $this->createForm(ChangePasswordType::class);

        $form->bind($request);
        $data = $form->getData();

        /** @var $factory \Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface */
        $factory = $this->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        $current = $encoder->encodePassword($data['current'], $user->getSalt());

        if ($current !== $user->getPassword()) {
            $form->get('current')->addError(new FormError('Current password is not correct'));
        }

        if ($form->isValid()) {
            $user->setPassword($encoder->encodePassword($data['password'], $user->getSalt()));

            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

            $this->get('session')->getFlashBag()->set('success', 'Your password has been changed.');

            return new JsonReloadResponse();
        }

        return new JsonErrorResponse($form);
    }
}


<?php
/**
 * This file is part of Vegas package
 *
 * @author Radosław Fąfara <radek@amsterdamstandard.com>
 * @copyright Amsterdam Standard Sp. Z o.o.
 * @homepage http://cmf.vegas
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Vegas\Media\Adapter;
use Phalcon\Image;

/**
 * Class Imagick
 * Provides compatibility for Vegas Media library users updating from older version due to WideImage replacement.
 * @package Vegas\Media\Adapter
 */
class Imagick extends Image\Adapter\Imagick
{
    /**
     * {@inheritdoc}
     */
    public function resize($width = null, $height = null, $master = null)
    {
        switch ($master) {
            case 'fill':
                $master = Image::TENSILE;
                break;
            case 'inside':
                $master = Image::AUTO;
                break;
            case 'outside':
                $master = Image::INVERSE;
                break;
            case 'precise':
                $master = Image::PRECISE;
                break;
        }
        return parent::resize($width, $height, $master);
    }

    /**
     * {@inheritdoc}
     */
    public function crop($width, $height, $offset_x = null, $offset_y = null)
    {
        switch ($offset_x) {
            case 'left':
                $offset_x = 0;
                break;
            case 'right':
                $offset_x = (int)($this->getWidth() - $width);
                break;
            case 'center':
                $offset_x = (int)(($this->getWidth() - $width) / 2);
                break;
        }
        switch ($offset_y) {
            case 'top':
                $offset_y = 0;
                break;
            case 'bottom':
                $offset_y = (int)($this->getHeight() - $height);
                break;
            case 'middle':
                $offset_y = (int)(($this->getHeight() - $height) / 2);
                break;
        }
        return parent::crop($width, $height, $offset_x, $offset_y);
    }

}


<?php
/**
 * @license MIT
 */
namespace Pivasic\Bundle\Common\Session;

use Pivasic\Bundle\Common\Bag\ValueBag;

/**
 * Class SessionBag
 * @package Pivasic\Bundle\Common\Session
 */
class SessionBag extends ValueBag
{
    public function __construct()
    {
        parent::__construct([]);
    }

    /**
     * Get true if the SESSION parameter is defined.
     *
     * @param string $key
     * @return bool true
     */
    public function has(string $key): bool
    {
        return isset($_SESSION) ? array_key_exists($key, $_SESSION) : false;
    }

    /**
     * Get the SESSION keys.
     *
     * @return array
     */
    public function keys(): array
    {
        return isset($_SESSION) ? array_keys($_SESSION) : [];
    }

    /**
     * @param string $key
     * @param mixed $value
     * @throws \LogicException
     */
    public function set($key, $value)
    {
        if (isset($_SESSION)) {
            $_SESSION[$key] = $value;
        } else {
            throw new \LogicException('Cannot set the value of an inactive session');
        }
    }

    /**
     * Returns a SESSION parameter by name.
     *
     * @param string $key
     * @param mixed|null $default The default value if parameter does not exist
     * @return mixed|null
     */
    public function fetch(string $key, $default = null)
    {
        return $_SESSION[$key] ?? $default;
    }

    /**
     * Returns the number of values.
     * @return int
     */
    public function count(): int
    {
        return isset($_SESSION) ? count($_SESSION) : 0;
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: inhere
 * Date: 2017/6/24
 * Time: 下午7:17
 */

namespace Toolkit\Pool;

/**
 * Class UnlimitedPool - 无(大小)限制的资源池，没有资源就创建
 * @package Inhere\Library\process
 */
class UnlimitedPool implements LitePoolInterface
{
    /**
     * @var \SplQueue
     */
    private $queue;

    /**
     * @var \Closure
     */
    private $creator;

    /**
     * @var int
     */
    private $maxSize;

    /**
     * class constructor.
     * @param int $maxSize
     */
    public function __construct(int $maxSize = 100)
    {
        $this->queue = new \SplQueue();
        $this->maxSize = $maxSize;
    }

    /**
     * @param \Closure $creator
     */
    public function setCreator(\Closure $creator): void
    {
        $this->creator = $creator;
    }

    /**
     * {@inheritdoc}
     * @throws \RuntimeException
     */
    public function get()
    {
        if (!$this->queue->isEmpty()) {
            return $this->queue->pop();
        }

        if ($this->maxSize > 0 && $this->count() >= $this->maxSize) {
            throw new \RuntimeException(
                "Server busy, no resources available.(The pool has been overflow max value: {$this->maxSize})"
            );
        }

        return ($this->creator)();
    }

    /**
     * @param $obj
     */
    public function put($obj)
    {
        $this->queue->push($obj);
    }

    /**
     * Empty the resource pool - Release all connections
     */
    public function clear()
    {
        while (!$this->queue->isEmpty()) {
            $this->queue->pop();
        }

        $this->queue = null;
    }

    /**
     * @return int
     */
    public function count(): int
    {
        return $this->queue->count();
    }

    /**
     * release pool
     */
    public function __destruct()
    {
        $this->clear();
    }

}


<?php
/**
 * @license MIT
 */
namespace Pivasic\Bundle\Common\Bag;

/**
 * Class ValueBag
 * @package Pivasic\Bundle\Common\Bag
 */
class ValueBag
{
    /**
     * @param array $bag
     */
    public function __construct(array $bag = [])
    {
        $this->bag = $bag;
    }

    /**
     * Returns true if parameter is defined.
     *
     * @param string $key
     * @return bool
     */
    public function has(string $key): bool
    {
        return array_key_exists($key, $this->bag);
    }

    /**
     * An array of parameter names.
     *
     * @return array
     */
    public function keys(): array
    {
        return array_keys($this->bag);
    }

    /**
     * Get value by parameter name.
     *
     * @param string $key
     * @param mixed|null $default The default value if parameter does not exist
     * @return mixed|null
     */
    public function fetch(string $key, $default = null)
    {
        $val = $this->bag[$key] ?? $default;
        if (!is_array($val)) {
            return trim($val);
        } else {
            return $val;
        }
    }

    /**
     * Get parameter value converted to integer.
     *
     * @param string $key
     * @param int $default The default value if parameter does not exist
     * @return int
     */
    public function fetchInt(string $key, int $default = 0): int
    {
        return intval($this->fetch($key, $default));
    }

    /**
     * Get parameter value converted to float with precision.
     *
     * @param string $key
     * @param float $default The default value if parameter does not exist
     * @param int $precision The optional number of decimal digits to round to
     * @return float
     */
    public function fetchFloat(string $key, float $default = 0.0, int $precision = 2): float
    {
        return round(floatval($this->fetch($key, $default)), $precision);
    }

    /**
     * Get parameter value converted to boolean.
     *
     * @param string $key
     * @param bool|false $default The default value if parameter does not exist
     * @return mixed
     */
    public function fetchBool(string $key, bool $default = false): bool
    {
        return $this->fetchFilter($key, $default, FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * Filter value.
     *
     * @param string $key
     * @param mixed|null $default The default value if parameter does not exist
     * @param int $filter         FILTER_* constant
     * @param array $options      Filter options
     * @see http://php.net/manual/en/function.filter-var.php
     * @return mixed
     */
    public function fetchFilter(string $key, $default = null, $filter = FILTER_DEFAULT, $options = [])
    {
        $value = $this->fetch($key, $default);

        if (!is_array($options) && $options) {
            $options = ['flags' => $options];
        }

        if (is_array($value) && !isset($options['flags'])) {
            $options['flags'] = FILTER_REQUIRE_ARRAY;
        }

        return filter_var($value, $filter, $options);
    }

    /**
     * Use custom function to process value.
     *
     * @param string $key
     * @param callable $callback
     * @param mixed|null $default
     * @return mixed
     */
    public function fetchCustom(string $key, $callback, $default = null)
    {
        return $callback($this->fetch($key, $default));
    }

    /**
     * Use mysql escape function.
     *
     * @param $key
     * @param \mysqli $db
     * @param string $default
     *
     * @return string
     */
    public function fetchEscape(string $key, \mysqli $db, string $default = ''): string
    {
        return $db->real_escape_string($this->fetch($key, $default));
    }

    /**
     * Returns the count of parameters.
     * @return int
     */
    public function count()
    {
        return count($this->bag);
    }

    private $bag;
}


<?php
/**
 * This file is part of keenly from.
 * @author brain_yang<qiaopi520@qq.com>
 * (c) brain_yang
 * github: https://github.com/keenlysoft/
 * @time 2018年5月27日
 * For the full copyright and license information, please view the LICENSE
 */
namespace keenly\routes;

use keenly\view\k;
/**
 * @name   keenly is php Frame
 * @method static get(['api'=>'home@api','getname'=>'home@getname']);
 * @method static post(['api'=>'home@api','getname'=>'home@getname']);
 * @method static api('request'['api'=>'home@api']);
 * @method static put(string $route, mixed $handler, array $opts = [])
 * @method static delete(string $route, mixed $handler, array $opts = [])
 * @method static options(string $route, mixed $handler, array $opts = [])
 * @method static head(string $route, mixed $handler, array $opts = [])
 * @method static search(string $route, mixed $handler, array $opts = [])
 * @method static connect(string $route, mixed $handler, array $opts = [])
 * @method static trace(string $route, mixed $handler, array $opts = [])
 * @method static any(string $route, mixed $handler, array $opts = [])
 * @author jack_yang
 * @link
 *
 */
class routes extends BaseRoutes{

    const req = 'API';

    const HTTP_Method = [
            'GET',
            'HEAD',
            'POST',
            'PUT',
            'DELETE',
            'CONNECT',
            'OPTIONS',
            'TRACE',
            'API'
    ];

    private static  $Rule = [];

    private static $Uri = [];

    private static $funcback = [];

    private static $PathParams = [];

    private static $Ruri;

    private static $Rmethod;

    private static $interest = [];

    public static function  __callstatic($method,$params){
        self::set_error_Model();
        if(strtoupper($method) == self::req){

        }else{
            $Rule = $method;
            $uri = strpos($params[0], DIRECTORY_SEPARATOR) === 0 ? $params[0] : DIRECTORY_SEPARATOR . $params[0];
            self::$PathParams[$uri][] = $params[1];
            $funcback = $params['1'];
            array_push(self::$PathParams[$uri], $method);
            array_push(self::$Rule, $Rule);
            array_push(self::$Uri, $uri);
            array_push(self::$funcback, $funcback);
        }

    }

    public  static  function  keenly(){
        self::$Ruri = parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH);
        self::$Rmethod = $_SERVER['REQUEST_METHOD'];
        self::RequestValidation(self::$Rmethod,self::$Ruri);
        if(in_array(strtolower(self::$Rmethod), self::$Rule) && in_array(strtoupper(self::$Rmethod), self::HTTP_Method)){
            if(in_array(strtolower(self::$Ruri), self::$Uri)){
                if(strpos(self::$PathParams[self::$Ruri]['0'], '@')){
                    $path = explode('@', self::$PathParams[self::$Ruri]['0']);
                    self::InstancePath($path);
                    return ;
                }
            }else{
                self::allRule();
                return ;
            }
        }
        self::error();
    }

    //@todo view web
    private static  function error(){
        header($_SERVER['SERVER_PROTOCOL']." 404 Not Found");
        k::render('404');
    }
    //
    private static function allRule(){
        if(in_array("/(:all)",self::$Uri)){
            $pathurl = mb_substr(self::$Ruri, 1);
            //if(strpos($pathurl,DIRECTORY_SEPARATOR)){
            $pathMethod = explode(DIRECTORY_SEPARATOR, $pathurl);
            self::InstancePath($pathMethod);
            return ;
            //}
        }
        self::error();
    }

    private static  function InstancePath($path){
        $c = self::$C['routes'];
        if(count($path) > 2 ){
            $class = $path['0'].'\\'.$c['routesName'].'\\'.$path['1'].$c['routesClass'];
            $fun = $path['2'];
        }else{
            $routesClass = (DIRECTORY_SEPARATOR == self::$Ruri)?
            $c['default_index'].'\\'.$c['routesName'].'\\'.$path['0'].$c['routesClass']:
            $c['default_index'].'\\'. $c['routesName'].'\\'.$path['0'].$c['routesClass'];//The default judgment index
            $class = $routesClass;
            $fun = empty($path['1'])?$c['default_index']:$path['1'];
        }
        self::call_fun($class, $fun);
    }

    private static  function call_fun($class,$fun){

        if(!class_exists($class)) {
            echo "controller not class ". $class;
            return ;
        }
        $control = self::interest($class);
        if (!method_exists($control, $fun)) {
            echo "controller and action not found";
            return ;
        } else {
            $control->$fun();
            return ;
        }
    }

    /**
     * @name func
     *
     * @param unknown $method
     * @param unknown $uri
     */
    private static function RequestValidation($method,$uri){
        if(strtolower($method) != (isset(self::$PathParams[$uri])?self::$PathParams[$uri]['1']:strtolower($method))){
            header($_SERVER['SERVER_PROTOCOL']." 404 Not Found");
            die("Request mode error");
        }
    }

    /**
     * @name interest instantiation class
     * @param classname $class
     * @return mixed|unknown
     */

    private static function  interest($class){
        $classNum = md5($class);
        if(isset(self::$interest[$classNum]) && !empty(self::$interest[$classNum])){
            return self::$interest[$classNum];
        }
        return self::$interest[$classNum] = new $class();
    }

}


<?php
/**
 * @license MIT
 */
namespace Pivasic\Core;

/**
 * Represents a HTTP request.
 *
 * Class Request
 * @package Pivasic\Core
 */
class Request
{
    /**
     * Request constructor.
     *
     * @param array $languageList
     * @param array $packageList
     * @param string $url
     */
    public function __construct(array &$languageList = [], array &$packageList = [], &$url = '')
    {
        if ('' == $url) {
            $url = filter_input_array(INPUT_SERVER)['REQUEST_URI'] ?? '';
        }
        $this->method = 'GET';
        $this->defaultLanguage = $languageList[0] ?? '';
        $this->language = $this->defaultLanguage;
        $this->package = 'Original';
        $this->route = 'index';

        $url = explode('?', $url);
        $url = trim(trim($url[0]), '/');
        if ('' != $url) {
            $partList = explode('/', $url);

            /**
             * Check language.
             */
            if (false !== ($key = array_search($partList[0], $languageList))) {
                unset($partList[0]);
                $partList = array_values($partList);
                $this->language = $languageList[$key];
            }

            /**
             * Check package.
             */
            if (isset($partList[0]) && false !== ($key = array_search(ucfirst($partList[0]), $packageList))) {
                unset($partList[0]);
                $partList = array_values($partList);
                $this->package = $packageList[$key];
            }

            /**
             * Get route.
             */
            if (isset($partList[0])) {
                $this->route = implode('/', $partList);
                $this->route = trim(str_replace('.', '_', $this->route), '/');
            }
        }

        /**
         * Request URL.
         */
        $this->url = '/' . $url;

        /**
         * Get method.
         */
        if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 'xmlhttprequest' == strtolower($_SERVER['HTTP_X_REQUESTED_WITH'])) {
            $this->method = 'AJAX';
        } else {
            if (isset($_SERVER['REQUEST_METHOD']) && in_array($_SERVER['REQUEST_METHOD'], ['GET', 'POST', 'HEAD', 'PUT', 'DELETE'])) {
                $this->method = $_SERVER['REQUEST_METHOD'];
            }
        }
    }

    /**
     * Path without an optional query.
     *
     * @return string
     */
    public function url(): string
    {
        return $this->url;
    }

    /**
     * Name of an app package.
     *
     * @return string
     */
    public function package(): string
    {
        return $this->package;
    }

    /**
     * URL path without language code, package name and an optional query parameters.
     *
     * @param bool $dropIndex
     * @return string
     */
    public function route(bool $dropIndex = false): string
    {
        if ($dropIndex) {
            if ('index' == $this->route) {
                return '';
            }
        }
        return $this->route;
    }

    /**
     * Request method.
     *
     * @return string
     */
    public function method(): string
    {
        return $this->method;
    }

    /**
     * Current language code.
     *
     * @return string
     */
    public function language(): string
    {
        return $this->language;
    }

    /**
     * Default language code.
     *
     * @return string
     */
    public function defaultLanguage(): string
    {
        return $this->defaultLanguage;
    }

    private $url;
    private $package;
    private $route;
    private $method;
    private $language;
    private $defaultLanguage;
}


<?php
/**
 * SQL statement generator.
 * Converts associative arrays and objects into partial SQL statements.
 *
 * @name        SqlStatement
 * @copyright   Copyright (c) 2018 Reid Woodbury Jr
 * @license     http://www.apache.org/licenses/LICENSE-2.0.html Apache License, Version 2.0
 */

namespace Diskerror\Typed;

use InvalidArgumentException;
use UnexpectedValueException;

/**
 * Class SqlStatement
 *
 * @package Diskerror\Typed
 */
class SqlStatement
{
	/**
	 * Ya don' need to make an instance of this class to use the static methods.
	 */
	protected function __construct()
	{
	}

	/**
	 * Returns a string formatted for an SQL insert or update.
	 *
	 * Accepts an associative array or Traversable object and
	 * an array where the values are the names of the desired keys.
	 * An empty "include" array means to use all.
	 *
	 * @param array|\stdClass $input
	 * @param array           $include
	 *
	 * @return string
	 */
	public static function toInsert($input, array $include = [])
	{
		if (is_object($input)) {
			if (method_exists($input, 'toArray')) {
				$input = $input->toArray();
			}
			elseif ($input instanceof \Traversable) {
				$input = iterator_to_array($input);
			}
			else {
				$input = (array)$input;
			}
		}
		elseif (!is_array($input)) {
			throw new InvalidArgumentException('input must be an associative array or traversable object');
		}

		if (count($include)) {
			$arr = [];
			foreach ($include as $i) {
				if (isset($input[$i])) {
					$arr[$i] &= $input[$i];
				}
			}
		}
		else {
			$arr = &$input;
		}

		$sqlStrs = [];
		foreach ($arr as $k => &$v) {
			$kEq = '`' . $k . '` = ';
			switch (gettype($v)) {
				case 'bool':
				case 'boolean':
					$sqlStrs[] = $kEq . ($v ? '1' : '0');
					break;

				case 'int':
				case 'integer':
				case 'float':
				case 'double':
					$sqlStrs[] = $kEq . $v;
					break;

				case 'string':
					//	if $v is a string that contains the text 'NULL' then
					if ($v === 'NULL') {
						$sqlStrs[] = $kEq . 'NULL';
					}
//					elseif ($v === '') {
//						//	This condition is required with bin2hex() as we can't use only '0x'.
//						$sqlStrs[] = $kEq . '""';
//				}
					else {
						$sqlStrs[] = $kEq . '"' . addslashes($v) . '"';
//						$sqlStrs[] = $kEq . '0x' . bin2hex($v);
					}
					break;

				case 'null':
				case 'NULL':
					//	if $v is a NULL
					$sqlStrs[] = $kEq . 'NULL';
					break;

				case 'object':
					if ($v instanceof DateTime) {
						$sqlStrs[] = $kEq . '"' . $v . '"';
						break;
					}
					//	other objects fall through
				case 'array':
					$sqlStrs[] = $kEq . '"' . addslashes(json_encode($v)) . '"';
//					$sqlStrs[] = $kEq . '0x' . bin2hex(json_encode($v));
					$jsonLastErr = json_last_error();
					if ($jsonLastErr !== JSON_ERROR_NONE) {
						throw new UnexpectedValueException(json_last_error_msg(), $jsonLastErr);
					}
					break;

				//	resource, just ignore these
				default:
					break;
			}
		}

		return implode(",\n", $sqlStrs);
	}

	/**
	 * Returns a string formatted for an SQL
	 * "ON DUPLICATE KEY UPDATE" statement.
	 *
	 * Accepts an associative array or Traversable object and
	 * an array where the values are the names of the desired keys.
	 * An empty "include" array means to use all.
	 *
	 * @param array|\stdClass $input
	 * @param array           $include
	 *
	 * @return string
	 */
	public static function toValues($input, array $include = [])
	{
		if (!is_array($input) && !is_object($input)) {
			throw new InvalidArgumentException('input must be an associative array or traversable object');
		}

		$sqlStrs = [];

		if (count($include)) {
			foreach ($include as $i) {
				if (array_key_exists($i, $input)) {
					$sqlStrs[] = '`' . $i . '` = VALUES(`' . $i . '`)';
				}
			}
		}
		else {
			foreach ($input as $k => &$v) {
				$sqlStrs[] = '`' . $k . '` = VALUES(`' . $k . '`)';
			}
		}

		return implode(",\n", $sqlStrs);
	}
}


<?php

namespace Jamosaur\Randstring;

class Randstring
{
    public $combinations = [];
    public $adjective;
    public $animal;
    public $number;
    private $adjectives;
    private $animals;
    private $min;
    private $max;
    private $case;
    private $maxLength;
    private $string;
    private $first;
    private $second;

    /**
     * Randstring constructor.
     *
     * @param null $case (ucwords, ucfirst|sentence, camel)
     * @param int  $maxLength
     * @param int  $min
     * @param int  $max
     */
    public function __construct($case = null, $maxLength = 100, $min = 1, $max = 99)
    {
        $this->case = $case;
        $this->maxLength = $maxLength;
        $this->min = $min;
        $this->max = $max;
        $this->adjectives = explode(PHP_EOL, file_get_contents(__DIR__ . '/dictionaries/adjectives.txt'));
        $this->animals = explode(PHP_EOL, file_get_contents(__DIR__ . '/dictionaries/animals.txt'));
    }

    /**
     * @return mixed
     */
    public function generate()
    {
        $this->generateString();
        if (strlen($this->string) > $this->maxLength) {
            return $this->generate();
        }

        return $this->string;
    }

    /**
     * Generate a string.
     */
    private function generateString()
    {
        $this->generateNumbers();
        $this->adjective = $this->adjectives[$this->first];
        $this->animal = $this->animals[$this->second];
        switch ($this->case) {
            case 'ucfirst':
                $this->string = ucfirst($this->adjective . $this->animal . $this->number);
                break;
            case 'ucwords':
            case 'sentence':
                $this->string = ucfirst($this->adjective) . ucfirst($this->animal) . ucfirst($this->number);
                break;
            case 'camel':
                $this->string = $this->adjective . ucfirst($this->animal) . $this->number;
                break;
            default:
                $this->string = $this->adjective . $this->animal . $this->number;
                break;
        }
    }

    /**
     * @param null $first
     * @param null $second
     */
    private function generateNumbers($first = null, $second = null)
    {
        $this->first = ($first) ? $first : mt_rand(0, count($this->adjectives) - 1);
        $this->second = ($second) ? $second : mt_rand(0, count($this->animals) - 1);
        $this->number = mt_rand($this->min, $this->max);
        if (array_key_exists($this->first . '.' . $this->second . '.' . $this->number, $this->combinations)) {
            $this->generateNumbers($this->first, $this->second);
        }
        $this->combinations[$this->first . '.' . $this->second . '.' . $this->number] = 1;
    }
}


<?php

/**
 * This file is part of Vegas package
 *
 * @author Adrian Malik <adrian.malik.89@gmail.com>
 * @copyright Amsterdam Standard Sp. Z o.o.
 * @homepage https://bitbucket.org/amsdard/vegas-phalcon
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Vegas\Upload;

use Vegas\Mvc\View;
use Vegas\Upload\Exception\ForbiddenFileExtensionException;
use Vegas\Upload\Exception\ForbiddenFileMimeTypeException;

trait Upload
{
    /**
     * Upload action for every controller which inherit from CRUDController
     *
     * @return mixed
     * @throws \Exception
     */
    public function uploadAction()
    {
        if ($this->request->hasFiles() == true) {
            $this->initializeScaffolding();
            $form = $this->scaffolding->getForm();
            $name = key($_FILES);
            /** @var \Vegas\Forms\Element\Upload $uploadElement */
            $uploadElement = $form->get($name);
            $model = $uploadElement->getModel();

            $path = $uploadElement->getPath();
            $maxFileSize = $uploadElement->getMaxFileSize();
            $minFileSize = $uploadElement->getMinFileSize();

            foreach ($this->request->getUploadedFiles() as $file) {
                $fileName = $file->getName();
                $fileSize = $file->getSize();
                $fileType = $file->getRealType();
                $fileExtensions = pathinfo($fileName, PATHINFO_EXTENSION);

                $allowed = $uploadElement->getAllowedExtensions();
                if (!empty($allowed)) {
                    if (!in_array($fileExtensions, $allowed)) {
                        throw new ForbiddenFileExtensionException();
                    }
                }

                $forbidden = $uploadElement->getForbiddenExtensions();
                if (!empty($forbidden)) {
                    if (in_array($fileExtensions, $forbidden)) {
                        throw new ForbiddenFileExtensionException();
                    }
                }

                $allowedMime = $uploadElement->getAllowedMimeTypes();
                if (!empty($allowedMime)) {
                    if (!in_array($fileType, $allowedMime)) {
                        throw new ForbiddenFileMimeTypeException();
                    }
                }

                $forbiddenMime = $uploadElement->getForbiddenMimeTypes();
                if (!empty($forbiddenMime)) {
                    if (in_array($fileType, $forbiddenMime)) {
                        throw new ForbiddenFileMimeTypeException();
                    }
                }

                if (!empty($maxFileSize)) {
                    if ($fileSize > $this->convertFileSizeToBytes($maxFileSize)) {
                        throw new \Exception('s');
                    }
                }

                if (!empty($minFileSize)) {
                    if ($fileSize < $this->convertFileSizeToBytes($minFileSize)) {
                        throw new \Exception('s');
                    }
                }

                if (empty($path)) {
                    $path = 'files/';
                }

                $model->name = $fileName;
                $model->mime_type = $fileType;
                $model->path = $path;
                $model->save();

                $file->moveTo($path . $model->_id);
                return $this->response->setJsonContent((string) $model->_id);
            }
        }
        $this->view->setRenderLevel(View::LEVEL_NO_RENDER);
    }

    private function convertFileSizeToBytes($size)
    {
        $size = (string) $size;
        $letters = '';
        $bytes = '';

        for ($i = 0; $i < strlen($size); $i++) {
            if (!is_numeric($size[$i])) {
                $letters .= $size[$i];
            } else {
                $bytes .= (string) $size[$i];
            }
        }

        $bytes = (int) $bytes;

        if ($letters) {
            switch (strtoupper($letters)) {
                case 'B': break;
                case 'KB': $bytes = $bytes * 1024; break;
                case 'MB': $bytes = $bytes * 1024 * 1024; break;
                case 'GB': $bytes = $bytes * 1024 * 1024 * 1024; break;
                case 'TB': $bytes = $bytes * 1024 * 1024 * 1024 * 1024; break;
                case 'PB': $bytes = $bytes * 1024 * 1024 * 1024 * 1024 * 1024; break;
            }
        }

        return $bytes;
    }
}


<?php
/**
 * This file is part of Vegas package
 *
 * @author Adrian Malik <adrian.malik.89@gmail.com>
 * @copyright Amsterdam Standard Sp. Z o.o.
 * @homepage https://bitbucket.org/amsdard/vegas-phalcon
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Vegas\Media;

use Vegas\Media\File\Exception as FileException;
use Vegas\Upload\Exception\CannotSaveFileInHardDriveException;

class Helper
{
    /**
     * Moves files from record (from "files" property of collection) from temporary destination
     * to the original destination.
     * Example of $record:
     *      {
     *          "_id": ObjectId("53195f675c65d1ba0b7b23c7"),
     *          "created_at": NumberInt(1394171751),
     *          "files": {                                <---- This line is the most important
     *              "0": {
     *                  "title": "Our Beautiful File",
     *                  "file_id": "53195f665c65d1ba0b7b23c6"
     *              }
     *          },
     *          "updated_at": NumberInt(1394171751)
     *      }
     *
     * @param \Vegas\Media\File\Decorator[] $files
     */
    public static final function moveFilesFrom($files)
    {
        foreach($files as $file) {
            self::moveFile($file->getRecord());
        }
    }

    /**
     * Move file from the temporary destination to the original destination
     *
     * @param \stdClass $file
     * @throws File\Exception
     */
    public static final function moveFile($file)
    {
        try {
            if($file->is_temp) {
                rename(
                    $file->temp_destination . '/' . $file->temp_name,
                    $file->original_destination . '/' . $file->temp_name
                );

                if(!file_exists($file->original_destination . '/' . $file->temp_name)) {
                    throw new CannotSaveFileInHardDriveException();
                }

                $file->is_temp = false;
                $file->expire = null;
                $file->save();
            }
        } catch (\Exception $exception) {
            throw new FileException($exception->getMessage());
        }
    }

    /**
     * Generates a thumbnails of the files associated with the record
     *
     * @param \Vegas\Media\File\Decorator[] $files
     * @param array $size
     */
    public static final function generateThumbnailsFrom($files, array $size)
    {
        foreach($files as $file) {
            self::generateThumbnail($file->getRecord(), $size);
        }
    }

    /**
     * Generates a thumbnails of the images in the following location:
     * {ORIGINAL_DESTINATION_OF_THE_FILE}/thumbnails/filename.{ext}
     *
     * @param \stdClass $file
     * @param array $size The resize parameters  (width, height, fit)
     * @param array $crop The cropping parameters (left, top)
     */
    public static final function generateThumbnail(
            $file,
            array $size = array('width' => 168, 'height' => 120, 'fit' => 'outside'),
            array $crop = array('left' => 'center', 'top' => 'middle')
    ) {
        if(!empty($file->original_destination) && isset($size['width']) && isset($size['height'])) {

            // ie. string(47) "/var/www/vegas/public/uploads/5326acd311dd4.jpg"
            $filePath = $file->original_destination . '/' . $file->temp_name;

            if (!is_dir($file->original_destination . '/thumbnails')) {
                mkdir($file->original_destination . '/thumbnails', 0777, true);
            }

            // Make sure we have a fit parameter
            if(!isset($size['fit'])) {
                $size['fit'] = 'outside';
            }

            // Make sure we have the crop parameters
            if(!isset($crop['left'])) {
                $crop['left'] = 'center';
            }
            if(!isset($crop['top'])) {
                $crop['top'] = 'middle';
            }

            // Use default output quality when not provided
            if (!isset($size['quality'])) {
                $size['quality'] = null;
            }

            $thumbFilePath = $file->original_destination . '/thumbnails/' . $size['width'] . '_' . $size['height'] . '_' . $file->temp_name;

            (new Adapter\Imagick($filePath))
                ->resize($size['width'], $size['height'], $size['fit'])
                ->crop($size['width'], $size['height'], $crop['left'], $crop['top'])
                ->save($thumbFilePath, $size['quality']);
        }
    }
}


<?php
/**
 * @license MIT
 */
namespace Pivasic\Bundle\Common\Bag;

/**
 * Class ServerBag
 * @package Pivasic\Bundle\Common\Bag
 */
class ServerBag extends ValueBag
{
    public function __construct()
    {
        parent::__construct($_SERVER);
    }

    /**
     * Returns a SERVER parameter by name.
     *
     * @param string $key
     * @param mixed|null $default The default value if the parameter key does not exist
     * @return mixed|null
     */
    public function fetch(string $key, $default = null)
    {
        if (isset($_SERVER[$key])) {
            return $_SERVER[$key];
        } else {
            $key = 'HTTP_' . $key;
            return $_SERVER[$key] ?? $default;
        }
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: inhere
 * Date: 2018-11-28
 * Time: 13:37
 */

namespace Toolkit\Pool;

/**
 * Class SPLQueuePool - pool
 * @package Toolkit\Pool
 */
abstract class SPLQueuePool extends AbstractPool
{
    /**
     * (Free) available resource queue
     * @var \SplQueue
     */
    protected $freeQueue;

    /**
     * (Busy) in use resource
     * @var \SplObjectStorage
     */
    protected $busyQueue;

    /**
     * init
     */
    protected function init()
    {
        $this->freeQueue = new \SplQueue();
        $this->busyQueue = new \SplObjectStorage();

        parent::init();
    }

    /**
     * 预(创建)准备资源
     * @param int $size
     * @return int
     */
    protected function prepare(int $size): int
    {
        if ($size <= 0) {
            return 0;
        }

        for ($i = 0; $i < $size; $i++) {
            $res = $this->create();
            // var_dump($i, $size, $res);
            $this->getFreeQueue()->push($res);
        }

        return $size;
    }

    /**
     * {@inheritdoc}
     * @return mixed
     * @throws \RuntimeException
     */
    public function get()
    {
        // There are also resources available
        if (!$this->freeQueue->isEmpty()) {
            $res = $this->freeQueue->pop();

            // add to busy pool
            $this->busyQueue->attach($res);

            return $res;
        }

        // No available free resources, and the resource pool is full. waiting ...
        if (!$this->hasFree() && ($this->count() >= $this->maxSize)) {
            if ($this->waitTimeout === 0) {
                // return null;
                throw new \RuntimeException(
                    "Server busy, no resources available.(The pool has been overflow max value: {$this->maxSize})"
                );
            }

            $res = $this->wait();

            // No resources available, resource pool is not full
        } else {
            // create new resource
            $this->prepare($this->stepSize);
            $res = $this->freeQueue->pop();
        }

        // add to busy pool
        $this->busyQueue->attach($res);

        return $res;
    }

    /**
     * 等待并返回可用资源
     * @return bool|mixed
     */
    abstract protected function wait();

    /**
     * {@inheritdoc}
     */
    public function put($resource)
    {
        // remove from busy queue
        $this->busyQueue->detach($resource);

        // push to free queue
        $this->freeQueue->push($resource);
    }

    /**
     * release pool
     */
    public function clear()
    {
        // clear free queue
        while ($obj = $this->getFreeQueue()->pop()) {
            $this->destroy($obj);
        }

        $this->busyQueue->removeAll($this->busyQueue);

        $this->freeQueue = null;
        $this->busyQueue = null;
    }

    /**
     * @return int
     */
    public function getFreeCount(): int
    {
        return $this->freeQueue->count();
    }

    /**
     * @return int
     */
    public function getBusyCount(): int
    {
        return $this->busyQueue->count();
    }

    /**
     * @return bool
     */
    public function hasFree(): bool
    {
        return $this->freeQueue->count() > 0;
    }

    /**
     * @return bool
     */
    public function hasBusy(): bool
    {
        return $this->busyQueue->count() > 0;
    }

    /**
     * @return \SplQueue
     */
    public function getFreeQueue(): \SplQueue
    {
        return $this->freeQueue;
    }

    /**
     * @return \SplObjectStorage
     */
    public function getBusyQueue(): \SplObjectStorage
    {
        return $this->busyQueue;
    }

}


<?php
/**
 * @license MIT
 */
namespace Pivasic\Core;

use Pivasic\Bundle\Debug\Wrap;
use Pivasic\Bundle\Template\Native;
use Pivasic\Core\Exception\RouteException;

/**
 * Initialize autoload.
 * Process request and get response.
 *
 * Class Application
 * @package Dspbee\Core
 */
class Application
{
    /**
     * Application constructor.
     *
     * @param string $appRoot
     */
    public function __construct(string $appRoot)
    {
        $appRoot = rtrim($appRoot, '/') . '/';
        $this->packageRoot = $appRoot;

        /**
         * Register autoload to app/package/$package/src dir's.
         */
        spl_autoload_register(function ($path) use ($appRoot) {
            $path = explode('\\', $path);
            array_shift($path);                                        // Vendor
            $package = $appRoot . 'package/' . array_shift($path);     // Package
            $path = $package . '/src/' . implode('/', $path) . '.php';
            if (file_exists($path)) {
                require_once $path;
            }
        });
    }

    /**
     * Process request.
     *
     * @param array $packageList
     * @param array $languageList
     * @param array $customRouteList
     * @param string $url
     *
     * @return Response
     * @throws RouteException
     */
    public function getResponse(array $packageList, array $languageList, array $customRouteList, string $url = ''): Response
    {
        $request = new Request($languageList, $packageList, $url);

        $this->packageRoot .= 'package/' . $request->package() . '/';

        try {
            /**
             * Process request.
             */
            if (isset($customRouteList[$request->package()])) {
                /**
                 * Path to custom router class.
                 */
                $path = $this->packageRoot . 'CustomRoute.php';
                if (file_exists($path)) {
                    require $path;
                    $route = $request->package() . '\\CustomRoute';
                    /**
                     * @var IRoute $route
                     */
                    $route = new $route();
                    $this->response = $route->getResponse($this->packageRoot, $request);
                    if ($this->response->is404()) {
                        $this->set404();
                    }
                    return $this->response;
                } else {
                    throw new RouteException(sprintf('The file "%s" does not exist', $path));
                }
            } else {
                $this->response = (new DefaultRoute())->getResponse($this->packageRoot, $request);
                if ($this->response->is404()) {
                    $this->set404();
                }
                return $this->response;
            }
        } catch (RouteException $e) {
            if (Wrap::isEnabled()) {
                throw $e;
            } else {
                $this->response = new Response();
                $this->set404();
                return $this->response;
            }
        }
    }

    /**
     * Set 404 code and content.
     */
    private function set404()
    {
        $this->response->setStatusCode(404);
        $content = '404 Not Found';
        if (file_exists($this->packageRoot . '/view/404.html.php')) {
            $content = (new Native($this->packageRoot))->getContent('404.html.php');
        }
        $this->response->setContent($content);
    }

    private $packageRoot;
    /**
     * @var Response
     */
    private $response;
}