<?php

namespace Ekyna\Component\Commerce\Invoice\Resolver;

use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Common\Util\Money;
use Ekyna\Component\Commerce\Invoice\Model as IM;
use Ekyna\Component\Commerce\Payment\Model as PM;

/**
 * Class InvoicePaymentResolver
 * @package Ekyna\Component\Commerce\Invoice\Resolver
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class InvoicePaymentResolver implements InvoicePaymentResolverInterface
{
    /**
     * @var array
     */
    protected $cache;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->clear();
    }

    /**
     * Clears the cache.
     */
    public function clear()
    {
        $this->cache = [];
    }

    /**
     * @inheritdoc
     */
    public function resolve(IM\InvoiceInterface $invoice)
    {
        $id = spl_object_id($invoice);

        if (!isset($this->cache[$id])) {
            $this->buildInvoicePayments($invoice->getSale());
        }

        return $this->cache[$id];
    }

    /**
     * Builds invoice payments for the given sale.
     *
     * @param SaleInterface $sale
     */
    protected function buildInvoicePayments(SaleInterface $sale)
    {
        $currency = $sale->getCurrency()->getCode(); // TODO Deal with currency conversions.
        $payments = $this->buildPaymentList($sale);
        /** @var IM\InvoiceSubjectInterface $sale */
        $invoices = $this->buildInvoiceList($sale);

        foreach ($invoices as $x => &$i) {
            $oid = spl_object_id($i['invoice']);
            $this->cache[$oid] = [];

            foreach ($payments as $y => &$p) {
                $r = new IM\InvoicePayment();
                $r->setPayment($p['payment']);

                $c = Money::compare($i['total'], $p['amount'], $currency);

                if (0 === $c) { // Equal
                    $r->setAmount($p['amount']);
                    $i['total'] = 0;
                    $p['amount'] = 0; unset($payments[$y]);
                } elseif (1 === $c) { // invoice > payment
                    $r->setAmount($p['amount']);
                    $i['total'] -= $p['amount'];
                    $p['amount'] = 0; unset($payments[$y]);
                } else { // payment > invoice
                    $r->setAmount($i['total']);
                    $p['amount'] -= $i['total'];
                    $i['total'] = 0;
                }

                $this->cache[$oid][] = $r;

                unset($p);
            }

            unset($i);
        }
    }

    /**
     * Builds the sale invoices list.
     *
     * @param IM\InvoiceSubjectInterface $subject
     *
     * @return array
     */
    protected function buildInvoiceList(IM\InvoiceSubjectInterface $subject)
    {
        $invoices = $subject->getInvoices(true)->toArray();

        usort($invoices, function (IM\InvoiceInterface $a, IM\InvoiceInterface $b) {
            return $a->getCreatedAt()->getTimestamp() - $b->getCreatedAt()->getTimestamp();
        });

        return array_map(function (IM\InvoiceInterface $invoice) {
            return [
                'invoice' => $invoice,
                'total'   => $invoice->getGrandTotal(),
            ];
        }, $invoices);
    }

    /**
     * Builds the sale payments list.
     *
     * @param PM\PaymentSubjectInterface $subject
     *
     * @return array
     */
    protected function buildPaymentList(PM\PaymentSubjectInterface $subject)
    {
        // TODO Deal with refund when implemented
        $payments = array_filter($subject->getPayments()->toArray(), function (PM\PaymentInterface $p) {
            if ($p->getMethod()->isOutstanding()) {
                return false;
            }

            if (!PM\PaymentStates::isPaidState($p->getState())) {
                return false;
            }

            return true;
        });

        usort($payments, function (PM\PaymentInterface $a, PM\PaymentInterface $b) {
            return $a->getCompletedAt()->getTimestamp() - $b->getCompletedAt()->getTimestamp();
        });

        // TODO Currency conversion

        return array_map(function (PM\PaymentInterface $payment) {
            return [
                'payment' => $payment,
                'amount'  => $payment->getAmount(),
            ];
        }, $payments);
    }
}


<?php

namespace Ekyna\Component\Commerce\Shipment\Builder;

use Ekyna\Component\Commerce\Common\Factory\SaleFactoryInterface;
use Ekyna\Component\Commerce\Common\Model\SaleItemInterface;
use Ekyna\Component\Commerce\Exception\LogicException;
use Ekyna\Component\Commerce\Shipment\Calculator\ShipmentCalculatorInterface;
use Ekyna\Component\Commerce\Shipment\Gateway\GatewayInterface;
use Ekyna\Component\Commerce\Shipment\Gateway\RegistryInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentItemInterface;

/**
 * Class ShipmentBuilder
 * @package Ekyna\Component\Commerce\Shipment\Builder
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class ShipmentBuilder implements ShipmentBuilderInterface
{
    /**
     * @var SaleFactoryInterface
     */
    private $factory;

    /**
     * @var RegistryInterface
     */
    private $registry;

    /**
     * @var ShipmentCalculatorInterface
     */
    private $calculator;

    /**
     * Constructor.
     *
     * @param SaleFactoryInterface        $factory
     * @param RegistryInterface           $registry
     * @param ShipmentCalculatorInterface $calculator
     */
    public function __construct(
        SaleFactoryInterface $factory,
        RegistryInterface $registry,
        ShipmentCalculatorInterface $calculator

    ) {
        $this->factory = $factory;
        $this->registry = $registry;
        $this->calculator = $calculator;
    }

    /**
     * @inheritdoc
     */
    public function build(ShipmentInterface $shipment)
    {
        if (null === $sale = $shipment->getSale()) {
            throw new LogicException("Sale must be set.");
        }

        if (!$shipment->isReturn()) {
            $shipment->setAutoInvoice(true);
        }

        $this->initializeMethod($shipment);
        $this->initializeRelayPoint($shipment);

        foreach ($sale->getItems() as $saleItem) {
            $this->buildItem($saleItem, $shipment);
        }
    }

    /**
     * Initializes the shipment's method.
     *
     * @param ShipmentInterface $shipment
     */
    private function initializeMethod(ShipmentInterface $shipment)
    {
        // Abort if shipment's method is defined
        if (null !== $shipment->getMethod()) {
            return;
        }

        $sale = $shipment->getSale();

        // Abort if default method is not defined
        if (null === $method = $sale->getShipmentMethod()) {
            return;
        }

        $gateway = $this->registry->getGateway($method->getGatewayName());

        // Set shipment method if supported
        if (!$shipment->isReturn() && $gateway->supports(GatewayInterface::CAPABILITY_SHIPMENT)) {
            $shipment->setMethod($method);

            return;
        }

        // Set return method if supported
        if ($shipment->isReturn() && $gateway->supports(GatewayInterface::CAPABILITY_RETURN)) {
            $shipment->setMethod($method);

            return;
        }
    }

    /**
     * Initializes the shipment's relay point.
     *
     * @param ShipmentInterface $shipment
     */
    private function initializeRelayPoint(ShipmentInterface $shipment)
    {
        // Abort if shipment method is not defined
        if (null === $method = $shipment->getMethod()) {
            // Clear the relay point if it is set
            if (null !== $shipment->getRelayPoint()) {
                $shipment->setRelayPoint(null);
            }

            return;
        }

        $gateway = $this->registry->getGateway($method->getGatewayName());

        // If gateway does not support relay point
        if (!$gateway->supports(GatewayInterface::CAPABILITY_RELAY)) {
            // Clear the relay point if it is set
            if (null !== $shipment->getRelayPoint()) {
                $shipment->setRelayPoint(null);
            }

            return;
        }

        // Set default relay point
        if (null !== $relayPoint = $shipment->getSale()->getRelayPoint()) {
            $shipment->setRelayPoint($relayPoint);
        }
    }

    /**
     * Builds the shipment item by pre populating quantity.
     *
     * @param SaleItemInterface $saleItem
     * @param ShipmentInterface $shipment
     *
     * @return ShipmentItemInterface|null
     */
    protected function buildItem(SaleItemInterface $saleItem, ShipmentInterface $shipment)
    {
        // If compound with only private children
        if ($saleItem->isCompound()) {
            // Resolve available and expected quantities by building children
            $available = $expected = null;
            foreach ($saleItem->getChildren() as $childSaleItem) {
                if (null !== $child = $this->buildItem($childSaleItem, $shipment)) {
                    $saleItemQty = $childSaleItem->getQuantity();

                    $e = $child->getExpected() / $saleItemQty;
                    if (null === $expected || $expected > $e) {
                        $expected = $e;
                    }

                    $a = $child->getAvailable() / $saleItemQty;
                    if (null === $available || $available > $a) {
                        $available = $a;
                    }
                }
            }

            // If any children is expected
            if (0 < $expected) {
                return $this->findOrCreateItem($shipment, $saleItem, $expected, $available);
            }

            return null;
        }

        $item = null;

        // Skip compound with only public children
        if (!($saleItem->isCompound() && !$saleItem->hasPrivateChildren())) {
            $expected = $shipment->isReturn()
                ? $this->calculator->calculateReturnableQuantity($saleItem, $shipment)
                : $this->calculator->calculateShippableQuantity($saleItem, $shipment);

            if (0 < $expected) {
                $item = $this->findOrCreateItem($shipment, $saleItem, $expected);
            }
        }

        // Build children
        if ($saleItem->hasChildren()) {
            foreach ($saleItem->getChildren() as $childSaleItem) {
                $this->buildItem($childSaleItem, $shipment);
            }
        }

        return $item;
    }

    /**
     * Finds or create the shipment item.
     *
     * @param ShipmentInterface $shipment
     * @param SaleItemInterface $saleItem
     * @param float             $expected
     * @param float             $available
     *
     * @return ShipmentItemInterface
     */
    private function findOrCreateItem(ShipmentInterface $shipment, SaleItemInterface $saleItem, $expected, $available = null)
    {
        $item = null;

        if (0 >= $expected) {
            return $item;
        }

        // Existing item lookup
        foreach ($shipment->getItems() as $i) {
            if ($i->getSaleItem() === $saleItem) {
                $item = $i;
                break;
            }
        }

        // Not found, create it
        if (null === $item) {
            $item = $this->factory->createItemForShipment($shipment);
            $item->setShipment($shipment);
            $item->setSaleItem($saleItem);
        }

        // Set expected quantity
        $item->setExpected($expected);

        if ($shipment->isReturn()) {
            // Set expected quantity as available
            $item->setAvailable($expected);
        } else {
            if (null === $available) {
                $available = $this->calculator->calculateAvailableQuantity($saleItem, $shipment);
            }

            // Set available quantity
            $item->setAvailable($available);

            // Set default quantity for new non return shipment items
            if (null === $shipment->getId()) {
                $item->setQuantity(min($expected, $available));
            }
        }

        return $item;
    }
}


<?php

namespace SnooPHP\Model;

use PDO;

/**
 * Wrapper for mysql database operations
 */
class Db
{
	/**
	 * Perform a generic query statement
	 *
	 * @param string	$queryString	query string
	 * @param array		$queryParams	query parameters
	 * @param string	$dbName			name of the db configuration (default master)
	 * @param bool		$fetchResults	if true (defaul behaviour) return query results, otherwise just true/false
	 *
	 * @return array|bool|int return query results or number of rows affected or false if fails
	 */
	public static function query($queryString, array $queryParams = [], $dbName = "master", $fetchResults = true)
	{
		// Prepare query
		$query = static::instance($dbName)->prepare($queryString);
		foreach ($queryParams as $column => $val) $query->bindValue(is_int($column) ? $column + 1 : ":".$column, $val);

		// Execute
		if ($query->execute())
			return $fetchResults ?
			$query->fetchAll(PDO::FETCH_ASSOC) :
			$query->rowCount();

		return false;
	}

	/**
	 * Begin database transaction
	 *
	 * @param string $dbName name of the db configuration (default master)
	 *
	 * @return bool
	 */
	public static function beginTransaction($dbName = "master")
	{
		return static::instance($dbName)->beginTransaction();
	}

	/**
	 * Commit transaction
	 *
	 * @param string $dbName name of the db configuration (default master)
	 *
	 * @return bool
	 */
	public static function commit($dbName = "master")
	{
		return static::instance($dbName)->commit();
	}

	/**
	 * Rollback transaction
	 *
	 * @param string $dbName name of the db configuration (default master)
	 *
	 * @return bool
	 */
	public static function rollBack($dbName = "master")
	{
		return static::instance($dbName)->rollBack();
	}

	/**
	 * Get last database error
	 *
	 * @param string $dbName name of the db configuration (default master)
	 *
	 * @return array
	 */
	public static function lastError($dbName = "master")
	{
		return static::instance($dbName)->errorInfo();
	}

	/**
	 * Get PDO instance
	 *
	 * @param string $dbName name of the database configuration
	 *
	 * @return PDO
	 */
	public static function instance($dbName = "master")
	{
		return $GLOBALS["db".ucwords($dbName)];
	}
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Repository;

use Doctrine\ORM\EntityRepository;
use Ekyna\Component\Commerce\Common\Model\SaleSources;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Stat\Entity\OrderStat;
use Ekyna\Component\Commerce\Stat\Repository\OrderStatRepositoryInterface;

/**
 * Class OrderStatRepository
 * @package Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Repository
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class OrderStatRepository extends EntityRepository implements OrderStatRepositoryInterface
{
    /**
     * @var \Doctrine\ORM\Query
     */
    private $revenueQuery;

    /**
     * @inheritDoc
     */
    public function findOneByDay(\DateTime $date)
    {
        return $this->findOneBy([
            'type' => OrderStat::TYPE_DAY,
            'date' => $date->format('Y-m-d'),
        ]);
    }

    /**
     * @inheritDoc
     */
    public function findOneByMonth(\DateTime $date)
    {
        return $this->findOneBy([
            'type' => OrderStat::TYPE_MONTH,
            'date' => $date->format('Y-m'),
        ]);
    }

    /**
     * @inheritDoc
     */
    public function findOneByYear(\DateTime $date)
    {
        return $this->findOneBy([
            'type' => OrderStat::TYPE_YEAR,
            'date' => $date->format('Y'),
        ]);
    }

    /**
     * @inheritDoc
     */
    public function findDayRevenuesByMonth(\DateTime $date, $detailed = false)
    {
        return $this->findRevenues(OrderStat::TYPE_DAY, $date, null, $detailed);
    }

    /**
     * @inheritDoc
     */
    public function findMonthRevenuesByYear(\DateTime $date, $detailed = false)
    {
        return $this->findRevenues(OrderStat::TYPE_MONTH, $date, null, $detailed);
    }

    /**
     * @inheritDoc
     */
    public function findYearRevenues($limit = 8, $detailed = false)
    {
        $qb = $this->createQueryBuilder('o');
        $expr = $qb->expr();

        $result = $qb
            ->select(['o.date', 'o.revenue', 'o.details'])
            ->andWhere($expr->eq('o.type', ':type'))
            ->addOrderBy('o.date')
            ->getQuery()
            ->setParameter('type', OrderStat::TYPE_YEAR)
            ->setMaxResults($limit = 8)
            ->getScalarResult();

        return $this->buildRevenueData($result, $detailed);
    }

    /**
     * Finds revenues.
     *
     * @param int            $type
     * @param \DateTime      $from
     * @param \DateTime|null $to
     * @param bool           $detailed
     *
     * @return array
     */
    private function findRevenues($type, \DateTime $from, \DateTime $to = null, $detailed = false)
    {
        if ($type === OrderStat::TYPE_DAY) {
            if (null === $to) {
                $from = (clone $from)->modify('first day of this month');
                $to = (clone $from)->modify('last day of this month');
            }
            $interval = new \DateInterval('P1D');
            $format = 'Y-m-d';
        } elseif ($type === OrderStat::TYPE_MONTH) {
            if (null === $to) {
                $from = (clone $from)->modify('first day of january ' . $from->format('Y'));
                $to = (clone $from)->modify('last day of december ' . $from->format('Y'));
            }
            $interval = new \DateInterval('P1M');
            $format = 'Y-m';
        } else {
            throw new InvalidArgumentException("Unexpected order stat type.");
        }

        $result = $this
            ->getRevenueQuery()
            ->setParameters([
                'type' => $type,
                'from' => $from->format($format),
                'to'   => $to->format($format),
            ])
            ->getScalarResult();

        $data = $this->buildRevenueData($result, $detailed);

        $period = new \DatePeriod($from, $interval, $to);

        $defaults = $detailed ? [] : 0;
        if ($detailed) {
            foreach (SaleSources::getSources() as $source) {
                $defaults[$source] = 0;
            }
        }

        /** @var \DateTime $d */
        foreach ($period as $d) {
            $index = $d->format($format);
            if (!isset($data[$index])) {
                $data[$index] = $defaults;
            };
        }
        ksort($data);

        return $data;
    }

    /**
     * Builds the revenue data.
     *
     * @param array $result
     * @param bool  $detailed
     *
     * @return array
     */
    private function buildRevenueData(array $result, $detailed = false)
    {
        $data = [];

        foreach ($result as $r) {
            $data[$r['date']] = $detailed ? json_decode($r['details'], true) : $r['revenue'];
        }

        return $data;
    }

    /**
     * Returns the revenues query.
     *
     * @return \Doctrine\ORM\AbstractQuery
     */
    private function getRevenueQuery()
    {
        if (null !== $this->revenueQuery) {
            return $this->revenueQuery;
        }

        $qb = $this->createQueryBuilder('o');
        $expr = $qb->expr();

        return $this->revenueQuery = $qb
            ->select(['o.date', 'o.revenue', 'o.details'])
            ->andWhere($expr->eq('o.type', ':type'))
            ->andWhere($expr->gte('o.date', ':from'))
            ->andWhere($expr->lte('o.date', ':to'))
            ->addOrderBy('o.date')
            ->getQuery();
    }
}


<?php

namespace PeskyCMF\Scaffold\DataGrid;

use Exceptions\Data\NotFoundException;
use PeskyCMF\Scaffold\AbstractValueViewer;
use PeskyCMF\Scaffold\MenuItem\CmfMenuItem;
use PeskyCMF\Scaffold\ScaffoldConfig;
use PeskyCMF\Scaffold\ScaffoldSectionConfig;
use PeskyORM\Core\DbExpr;
use PeskyORM\ORM\Column;
use PeskyORM\ORM\TableInterface;
use Swayok\Html\Tag;
use Swayok\Utils\ValidateValue;

class DataGridConfig extends ScaffoldSectionConfig {

    const ROW_ACTIONS_COLUMN_NAME = '__actions';

    protected $allowRelationsInValueViewers = true;

    protected $allowComplexValueViewerNames = true;

    protected $template = 'cmf::scaffold.datagrid';
    /**
     * @var int
     */
    protected $recordsPerPage = 25;
    /**
     * @var int
     */
    protected $offset = 0;
    /**
     * @var int
     */
    protected $maxLimit = 100;
    /**
     * @var string
     */
    protected $orderBy = null;
    /**
     * @var string
     */
    protected $orderDirection = self::ORDER_ASC;
    const ORDER_ASC = 'asc';
    const ORDER_DESC = 'desc';
    const ORDER_ASC_NULLS_FIRST = 'asc nulls first';
    const ORDER_ASC_NULLS_LAST = 'asc nulls last';
    const ORDER_DESC_NULLS_FIRST = 'desc nulls first';
    const ORDER_DESC_NULLS_LAST = 'desc nulls last';

    static protected $orderOptions = [
        self::ORDER_ASC,
        self::ORDER_DESC,
        self::ORDER_ASC_NULLS_FIRST,
        self::ORDER_ASC_NULLS_LAST,
        self::ORDER_DESC_NULLS_FIRST,
        self::ORDER_DESC_NULLS_LAST,
    ];
    /**
     * Add a checkboxes column to datagrid so user can select several rows and perform bulk-actions
     * @var bool
     */
    protected $allowMultiRowSelection = false;
    /** @var bool */
    protected $allowBulkItemsEditing = false;
    /** @var bool */
    protected $allowBulkItemsDelete = true;
    /** @var bool */
    protected $allowFilteredItemsEditing = false;
    /** @var bool */
    protected $allowFilteredItemsDelete = false;
    /** @var \Closure|null */
    protected $bulkActionsToolbarItems;
    /** @var bool */
    protected $isRowActionsEnabled = true;
    /** @var \Closure|null */
    protected $rowActions;
    /** @var array */
    protected $additionalDataTablesConfig = [];
    /** @var bool */
    protected $isRowActionsColumnFixed = true;
    /** @var bool */
    protected $isFilterOpened = false;
    /** @var string|null */
    protected $enableNestedViewBasedOnColumn;
    /** @var int */
    protected $nestedViewsDepthLimit = -1;
    /** @var array */
    protected $rowsPositioningColumns = [];
    /** @var int|float */
    protected $rowsPosittioningStep = 100;
    /** @var \Closure|null */
    protected $contextMenuItems;
    /** @var bool */
    protected $isContextMenuEnabled = true;
    /** @var bool */
    protected $isMultiRowSelectionColumnFixed = true;
    /** @var array */
    protected $additionalViewsForTemplate = [];
    /** @var DataGridRendererHelper|null */
    protected $rendererHelper;

    public function __construct(TableInterface $table, ScaffoldConfig $scaffoldConfig) {
        parent::__construct($table, $scaffoldConfig);
        $this->recordsPerPage = $scaffoldConfig::getCmfConfig()->rows_per_page();
        $this->setOrderBy($table->getPkColumnName());
    }

    /**
     * Alias for setValueViewers
     * @param array $datagridColumns
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \BadMethodCallException
     * @throws \PeskyCMF\Scaffold\ScaffoldException
     * @throws \InvalidArgumentException
     */
    public function setColumns(array $datagridColumns) {
        return $this->setValueViewers($datagridColumns);
    }

    /**
     * @return DataGridColumn[]|AbstractValueViewer[]
     */
    public function getDataGridColumns() {
        return $this->getValueViewers();
    }

    /**
     * @param string $name
     * @return bool
     */
    public function hasDataGridColumn($name) {
        return $this->hasValueViewer($name);
    }

    protected function createValueRenderer() {
        return DataGridCellRenderer::create();
    }

    /**
     * @param array $columnNames
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \BadMethodCallException
     * @throws \InvalidArgumentException
     * @throws \PeskyCMF\Scaffold\ScaffoldException
     * @throws \PeskyCMF\Scaffold\ScaffoldSectionConfigException
     */
    public function setInvisibleColumns(...$columnNames) {
        return call_user_func_array(array($this, 'setAdditionalColumnsToSelect'), $columnNames);
    }

    /**
     * Mimics setInvisibleColumns()
     * @param array $columnNames
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \PeskyCMF\Scaffold\ScaffoldException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    public function setAdditionalColumnsToSelect(...$columnNames) {
        parent::setAdditionalColumnsToSelect($columnNames);
        foreach ($this->getAdditionalColumnsToSelect() as $name) {
            $this->addValueViewer($name, DataGridColumn::create()->setIsVisible(false));
        }
        return $this;
    }

    /**
     * @param AbstractValueViewer|DataGridColumn $viewer
     * @return int
     */
    protected function getNextValueViewerPosition(AbstractValueViewer $viewer) {
        if ($viewer->isVisible()) {
            /** @var DataGridColumn $otherValueViewer */
            $count = 0;
            foreach ($this->valueViewers as $otherValueViewer) {
                if ($otherValueViewer->isVisible()) {
                    $count++;
                }
            }
            return $count;
        } else {
            return -1;
        }
    }

    /**
     * @param bool $shown - true: filter will be opened on data grid load | false: filter will be hidden
     * @return $this
     */
    public function setFilterIsOpenedByDefault($shown = true) {
        $this->isFilterOpened = $shown;
        return $this;
    }

    /**
     * @return $this
     */
    public function openFilterByDefault() {
        $this->isFilterOpened = true;
        return $this;
    }

    /**
     * @return $this
     */
    public function closeFilterByDefault() {
        $this->isFilterOpened = false;
        return $this;
    }

    /**
     * @return bool
     */
    public function isFilterOpenedByDefault() {
        return $this->isFilterOpened;
    }

    /**
     * @return int
     */
    public function getRecordsPerPage() {
        return $this->recordsPerPage;
    }

    /**
     * @param int $recordsPerPage
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setRecordsPerPage($recordsPerPage) {
        if (!ValidateValue::isInteger($recordsPerPage, true)) {
            throw new \InvalidArgumentException('$recordsPerPage argument must be an integer value');
        }
        $this->recordsPerPage = min($this->maxLimit, $recordsPerPage);
        return $this;
    }

    /**
     * @return int
     */
    public function getOffset() {
        return $this->offset;
    }

    /**
     * @param int $offset
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setOffset($offset) {
        if (!ValidateValue::isInteger($offset, true)) {
            throw new \InvalidArgumentException('$offset argument must be an integer value');
        }
        $this->offset = max($offset, 0);
        return $this;
    }

    /**
     * @return string
     */
    public function getOrderBy() {
        return $this->orderBy;
    }

    /**
     * @param string $orderBy
     * @param null $direction - 'asc', 'desc', 'asc nulls first' or 'desc nulls last' in any case
     * @return $this
     * @throws \BadMethodCallException
     * @throws \InvalidArgumentException
     */
    public function setOrderBy($orderBy, $direction = null) {
        if (!($orderBy instanceof DbExpr) && !$this->getTable()->getTableStructure()->hasColumn($orderBy)) {
            throw new \InvalidArgumentException("Table {$this->getTable()->getName()} has no column [$orderBy]");
        }
        if ($orderBy instanceof DbExpr) {
            $orderBy->setWrapInBrackets(false);
            if (empty($direction)) {
                $this->orderDirection = null;
            }
        }
        if (!empty($direction)) {
            $this->setOrderDirection($direction);
        }
        $this->orderBy = $orderBy;
        return $this;
    }

    /**
     * @return string
     */
    public function getOrderDirection() {
        return $this->orderDirection;
    }

    /**
     * @param string $orderDirection
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setOrderDirection($orderDirection) {
        $orderDirection = strtolower($orderDirection);
        if (!in_array($orderDirection, static::$orderOptions, true)) {
            throw new \InvalidArgumentException(
                "Invalid order direction [$orderDirection]. Expected one of: " . implode(', ', static::$orderOptions)
            );
        }
        $this->orderDirection = $orderDirection;
        return $this;
    }

    /**
     * @return bool
     */
    public function isAllowedMultiRowSelection() {
        return $this->allowMultiRowSelection;
    }

    /**
     * @param bool $allowMultiRowSelection
     * @return $this
     */
    public function setMultiRowSelection($allowMultiRowSelection) {
        $this->allowMultiRowSelection = (bool)$allowMultiRowSelection;
        return $this;
    }

    /**
     * Pass 'true' to fix/stick multi-row selection column in data grid so it will not move during
     * horisontal scrolling of data grid
     * @param bool $isFixed
     * @return $this
     */
    public function setIsMultiRowSelectionColumnFixed($isFixed) {
        $this->isMultiRowSelectionColumnFixed = (bool)$isFixed;
        return $this;
    }

    /**
     * @return bool
     */
    public function isMultiRowSelectionColumnFixed() {
        return $this->isMultiRowSelectionColumnFixed;
    }

    /**
     * @param bool $isAllowed - default: true
     * @return $this
     */
    public function setIsBulkItemsDeleteAllowed($isAllowed) {
        $this->allowBulkItemsDelete = (bool)$isAllowed;
        $this->setMultiRowSelection(true);
        return $this;
    }

    /**
     * @return bool
     */
    public function isBulkItemsDeleteAllowed() {
        return $this->allowBulkItemsDelete;
    }

    /**
     * Bulk editable columns provided via FormConfig->setBulkEditableColumns() or FormConfig->addBulkEditableColumns()
     * @param bool $isAllowed - default: false
     * @return $this
     */
    public function setIsBulkItemsEditingAllowed($isAllowed) {
        $this->allowBulkItemsEditing = (bool)$isAllowed;
        $this->setMultiRowSelection(true);
        return $this;
    }

     /**
     * @return bool
     */
    public function isBulkItemsEditingAllowed() {
        return $this->allowBulkItemsEditing;
    }

    /**
     * @param bool $isAllowed - default: false
     * @return $this
     */
    public function setIsFilteredItemsDeleteAllowed($isAllowed) {
        $this->allowFilteredItemsDelete = (bool)$isAllowed;
        return $this;
    }

     /**
     * @return bool
     */
    public function isFilteredItemsDeleteAllowed() {
        return $this->allowFilteredItemsDelete;
    }

    /**
     * @param bool $isAllowed - default: false
     * @return $this
     */
    public function setIsFilteredItemsEditingAllowed($isAllowed) {
        $this->allowFilteredItemsEditing = (bool)$isAllowed;
        return $this;
    }

     /**
     * @return bool
     */
    public function isFilteredItemsEditingAllowed() {
        return $this->allowFilteredItemsEditing;
    }

    /**
     * @return string[]
     * @throws \UnexpectedValueException
     */
    public function getBulkActionsToolbarItems() {
        if (empty($this->bulkActionsToolbarItems)) {
            return [];
        }
        $bulkActionsToolbarItems = call_user_func($this->bulkActionsToolbarItems, $this);
        if (!is_array($bulkActionsToolbarItems)) {
            throw new \UnexpectedValueException(get_class($this) . '->bulkActionsToolbarItems closure must return an array');
        }
        /** @var Tag|string $item */
        /** @var array $bulkActionsToolbarItems */
        foreach ($bulkActionsToolbarItems as &$item) {
            if (is_object($item)) {
                if ($item instanceof CmfMenuItem) {
                    // do nothing
                } else if (method_exists($item, 'build')) {
                    $item = $item->build();
                } else if (method_exists($item, '__toString')) {
                    $item = (string) $item;
                } else {
                    throw new \UnexpectedValueException(
                        get_class($this) . '->bulkActionsToolbarItems: array may contain only strings and objects with build() or __toString() methods'
                    );
                }
            } else if (!is_string($item)) {
                throw new \UnexpectedValueException(
                    get_class($this) . '->bulkActionsToolbarItems: array may contain only strings and objects with build() or __toString() methods'
                );
            }
        }
        return $bulkActionsToolbarItems;
    }

    /**
     * @param \Closure $callback - function (ScaffoldSectionConfig $scaffoldSectionConfig) { return []; }
     * Callback must return an array.
     * Array may contain only strings, Tag class instances, or any object with build() or __toString() method
     * Examples:
     * - call some url via ajax passing all selected ids and then run "callback(json)"
        CmfRequestMenuItem::bulkActionOnSelectedRows(static::getUrlCustomAction('bulk_action_name'), 'delete')
            // use ':count' in setTitle() to insert selected items count
            ->setTitle(':count ' . $this->translate('datagrid.bulk_action.action_name'))
            ->setTooltip($this->translate('datagrid.bulk_action.action_name_tooltip'))
            // ask user to confirm action
            ->setConfirm($this->translate('datagrid.bulk_action.action_name_confirmation'))
            // use this when you need to receive data from column other then primary key (default: current table's primary key column name)
            ->setPrimaryKeyColumnName('parent_id')
            // one of: json, html, xml. Default: 'json' (may be useful for your custom response handler)
            ->setResponseDataType('json')
            // set custom response handler function.
            // callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
            // It will receive 3 args: data, $link, defaultOnSuccessCallback
            ->setOnSuccess('callbackFuncitonName');
     * or
        Tag::li(
            Tag::a()
                ->setContent(trans('path.to.translation'))
                //^ you can use ':count' in label to insert selected items count
                ->setDataAttr('action', 'bulk-selected')
                ->setDataAttr('confirm', trans('path.to.translation'))
                //^ confirm action before sending request to server
                ->setDataAttr('url', cmfRoute('route', [], false))
                ->setDataAttr('method', 'delete')
                //^ can be 'post', 'put', 'delete' depending on action type
                ->setDataAttr('id-field', 'id')
                //^ id field name to use to get rows ids, default: 'id'
                ->setDataAttr('on-success', 'callbackFuncitonName')
                //^ callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
                //^ It will receive 3 args: data, $link, defaultOnSuccessCallback
                ->setDataAttr('response-type', 'json')
                //^ one of: json, html, xml. Default: 'json'
                ->setHref('javascript: void(0)');
        )
     * Values will be received in the 'ids' key of the request as array
     * - call some url via ajax passing filter conditions and then run "callback(json)"
        CmfRequestMenuItem::bulkActionOnFilteredRows(static::getUrlCustomAction('bulk_action_name'), 'put')
            // use ':count' in setTitle() to insert selected items count
            ->setTitle(':count ' . $this->translate('datagrid.bulk_action.action_name'))
            ->setTooltip($this->translate('datagrid.bulk_action.action_name_tooltip'))
            // ask user to confirm action
            ->setConfirm($this->translate('datagrid.bulk_action.action_name_confirmation'))
            // one of: json, html, xml. Default: 'json' (may be useful for your custom response handler)
            ->setResponseDataType('json')
            // set custom response handler function.
            // callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
            // It will receive 3 args: data, $link, defaultOnSuccessCallback
            ->setOnSuccess('callbackFuncitonName');
     * or
        Tag::li(
            Tag::a()
                ->setContent(trans('path.to.translation'))
                //^ you can use ':count' in label to insert filtered items count
                ->setDataAttr('action', 'bulk-filtered')
                ->setDataAttr('confirm', trans('path.to.translation'))
                //^ confirm action before sending request to server
                ->setDataAttr('url', cmfRoute('route', [], false))
                ->setDataAttr('method', 'put')
                //^ can be 'post', 'put', 'delete' depending on action type
                ->setDataAttr('on-success', 'callbackFuncitonName')
                //^ callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
                //^ It will receive 3 args: data, $link, defaultOnSuccessCallback
                ->setDataAttr('response-type', 'json')
                //^ one of: json, html, xml. Default: 'json'
                ->setHref('javascript: void(0)');
        )
     * - bulk actions with custom on-click handler
        Tag::li(
            Tag::button()
                ->setContent(trans('path.to.translation'))
                //^ you can use ':count' in label to insert selected items count or filtered items count
                //^ depending on 'data-type' attribute
                ->setClass('btn btn-success')
                ->setDataAttr('type', 'bulk-selected')
                //^ 'bulk-selected' or 'bulk-filtered'
                ->setDataAttr('url', cmfRoute('route', [], false))
                ->setDataAttr('id-field', 'id')
                //^ id field name to use to get rows ids, default: 'id'
                ->setOnClick('someFunction(this)')
                //^ for 'bulk-selected': inside someFunction() you can get selected rows ids via $(this).data('data').ids
        )
     * Conditions will be received in the 'conditions' key of the request as JSON string
     * @return $this
     */
    public function setBulkActionsToolbarItems(\Closure $callback) {
        $this->bulkActionsToolbarItems = $callback;
        return $this;
    }

    /**
     * @param array $records
     * @param array $virtualColumns - list of columns that are provided in TableStructure but marked as not existing in DB
     * @return array
     */
    public function prepareRecords(array $records, array $virtualColumns = []) {
        foreach ($records as $idx => &$record) {
            $record = $this->prepareRecord($record, $virtualColumns);
            $resourceName = $this->getScaffoldConfig()->getResourceName();
            if (array_get($record, '___details_allowed', false)) {
                $record['___details_url'] = routeToCmfItemDetails($resourceName, $record['___pk_value']);
            }
            if (array_get($record, '___edit_allowed', false)) {
                $record['___edit_url'] = routeToCmfItemEditForm($resourceName, $record['___pk_value']);
            }
            if (array_get($record, '___delete_allowed', false)) {
                $record['___delete_url'] = routeToCmfItemDelete($resourceName, $record['___pk_value']);
            }
            if (array_get($record, '___cloning_allowed', false)) {
                $record['___clone_url'] = routeToCmfItemCloneForm($resourceName, $record['___pk_value']);
            }
            $record['___max_nesting_depth'] = $this->getNestedViewsDepthLimit();
        }
        return $records;
    }

    /**
     * @inheritdoc
     */
    public function createValueViewer() {
        return DataGridColumn::create();
    }

    /**
     * @return Tag[]|string[]
     */
    public function getRowActions() {
        return $this->rowActions ? (array)call_user_func($this->rowActions, $this) : [];
    }

    /**
     * Note: common actions: 'details', 'edit', 'clone', 'delete' will be added automatically before custom menu items.
     * You can manipulate positioning of common items using actions names as keys (ex: 'details' => null).
     * @param \Closure $rowActionsBuilder - function (ScaffolSectionConfig $scaffoldSectionConfig) { return []; }
     * Examples:
     * 1. RowAction
     * a. Preferred usage:
     * - CmfMenuItem::redirect(cmfRoute('route', [], false))
            ->setTitle($this->translate('action.details'))
            ->setIconClasses('fa fa-user text-primary')
     * - CmfMenuItem::request(cmfRoute('route', [], false), 'delete')
            ->setTitle($this->translate('action.delete'))
            ->setIconClasses('fa fa-trash text-danger')
            ->setConfirm($this->translate('message.delete_confirm'));
     * b. Alternative usage:
     * - call some url via ajax blocking data grid while waiting for response and then run "callback(json)"
        * Tag::a()
            ->setContent('')
            ->setClass('row-action text-success')
            ->setTitle(cmfTransCustom('.path.to.translation'))
            ->setDataAttr('toggle', 'tooltip')
            ->setDataAttr('container', '#section-content .content') //< tooltip container
            ->setDataAttr('block-datagrid', '1')
            ->setDataAttr('action', 'request')
            ->setDataAttr('method', 'put')
            ->setDataAttr('url', cmfRoute('route', [], false))
            ->setDataAttr('data', 'id={{= it.id }}')
            //->setDataAttr('url', cmfRouteTpl('route', [], ['id'], false))
            ->setDataAttr('on-success', 'callbackFuncitonName')
            //^ callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
            //^ It will receive 3 args: data, $link, defaultOnSuccessCallback
            ->setHref('javascript: void(0)')
            ->build()
     * - redirect
        * Tag::a()
            ->setContent('')
            ->setClass('row-action text-primary')
            ->setTitle(cmfTransCustom('.path.to.translation'))
            ->setDataAttr('toggle', 'tooltip')
            ->setDataAttr('container', '#section-content .content') //< tooltip container
            ->setHref(cmfRoute('route', [], false))
            ->setTarget('_blank')
            ->build()
     * 2. List of row actions:
     *      [
     *          RowAction1,
     *          RowAction2,
     *          'delete' => null
     *      ]
     * @return $this
     */
    public function setRowActions(\Closure $rowActionsBuilder) {
        $this->rowActions = $rowActionsBuilder;
        return $this;
    }

    /**
     * @param bool $isEnabled
     * @return $this
     */
    public function setIsRowActionsEnabled($isEnabled) {
        $this->isRowActionsEnabled = (bool)$isEnabled;
        return $this;
    }

    /**
     * @return bool
     */
    public function isRowActionsEnabled() {
        return $this->isRowActionsEnabled;
    }

    /**
     * @return bool
     */
    public function isRowActionsColumnFixed() {
        return $this->isRowActionsColumnFixed;
    }

    /**
     * Pass 'true' to fix/stick actions column in data grid so it will not move during
     * horisontal scrolling of data grid
     * @param bool $isFixed
     * @return $this
     */
    public function setIsRowActionsColumnFixed(bool $isFixed) {
        $this->isRowActionsColumnFixed = $isFixed;
        return $this;
    }

    /**
     * @param bool $isEnabled
     * @return $this
     */
    public function setIsContextMenuEnabled(bool $isEnabled) {
        $this->isContextMenuEnabled = $isEnabled;
        return $this;
    }

    /**
     * @return bool
     */
    public function isContextMenuEnabled() {
        return $this->isContextMenuEnabled;
    }

    /**
     * @return array
     */
    public function getContextMenuItems() {
        return $this->contextMenuItems ? (array)call_user_func($this->contextMenuItems, $this) : [];
    }

    /**
     * Note: common actions: 'details', 'edit', 'clone', 'delete' will be added automatically before custom
     * menu items. You can manipulate positioning of common items using action name as key and null as value
     * (ex: 'details' => null) instead of CmfMenuItem or Tag.
     * @param \Closure $contextMenuItems - function (ScaffolSectionConfig $scaffoldSectionConfig) { return []; }
     * Format:
     * 1. MenuItem
     * - CmfMenuItem::redirect(cmfRoute('route', [], false))
            ->setTitle($this->translate('action.details'))
            ->setIconClasses('fa fa-user text-primary')
     * - CmfMenuItem::request(cmfRoute('route', [], false), 'delete')
            ->setTitle($this->translate('action.delete'))
            ->setIconClasses('fa fa-trash text-danger')
            ->setConfirm($this->translate('message.delete_confirm'));
     * - Tag::li(Tag::a()) (For Tag::a() format see setRowActions() docs)
     * 2. List of menu items:
     * - No grouping:
            [
                MenuItem1,
                MenuItem2,
                'edit' => null
                ...
            ]
     * - With groups:
            [
                [
                    MenuItem1,
                    MenuItem2,
                    ...
                ],
                [
                    MenuItem3,
                    'delete' => null
                ]
            ]
     * @return $this
     */
    public function setContextMenuItems(\Closure $contextMenuItems) {
        $this->setIsContextMenuEnabled(true);
        $this->contextMenuItems = $contextMenuItems;
        return $this;
    }

    /**
     * @return array
     */
    public function getAdditionalDataTablesConfig() {
        return $this->additionalDataTablesConfig;
    }

    /**
     * @param array $additionalDataTablesConfig
     * @return $this
     */
    public function setAdditionalDataTablesConfig(array $additionalDataTablesConfig) {
        $this->additionalDataTablesConfig = $additionalDataTablesConfig;
        return $this;
    }

    /**
     * @param string $name
     * @param null|DataGridColumn|AbstractValueViewer $tableCell
     * @return ScaffoldSectionConfig
     */
    public function addValueViewer($name, AbstractValueViewer $tableCell = null) {
        $tableCell = !$tableCell && $name === static::ROW_ACTIONS_COLUMN_NAME
            ? $this->getTableCellForForRowActions()
            : $tableCell;
        return parent::addValueViewer($name, $tableCell);
    }

    /**
     * @return DataGridColumn
     */
    protected function getTableCellForForRowActions() {
        return DataGridColumn::create()
            ->setIsLinkedToDbColumn(false)
            ->setName(static::ROW_ACTIONS_COLUMN_NAME)
            ->setLabel($this->translateGeneral('actions.column_label'))
            ->setType(DataGridColumn::TYPE_STRING);
    }

    /**
     * Finish building config.
     * This may trigger some actions that should be applied after all configurations were provided
     */
    public function finish() {
        parent::finish();
        if ($this->isRowActionsEnabled() && !$this->hasValueViewer(static::ROW_ACTIONS_COLUMN_NAME)) {
            $this->addValueViewer(static::ROW_ACTIONS_COLUMN_NAME, null);
        }
        if ($this->isNestedViewEnabled() && !$this->hasValueViewer($this->getColumnNameForNestedView())) {
            $this->addValueViewer($this->getColumnNameForNestedView(), DataGridColumn::create()->setIsVisible(false));
        }
        if ($this->isRowsReorderingEnabled()) {
            $reorderingColumns = $this->getRowsPositioningColumns();
            $allowedColumnTypes = [Column::TYPE_INT, Column::TYPE_FLOAT, Column::TYPE_UNIX_TIMESTAMP];
            foreach ($reorderingColumns as $columnName) {
                if (!$this->hasValueViewer($columnName)) {
                    throw new NotFoundException(
                        "Column '$columnName' provided for reordering was not found within declared data grid columns"
                    );
                }
                $valueViewer = $this->getValueViewer($columnName);
                if (!$valueViewer->isLinkedToDbColumn() && $valueViewer->getTableColumn()->isItExistsInDb()) {
                    throw new \UnexpectedValueException(
                        "Column '$columnName' provided for reordering must be linked to a column that exists in database"
                    );
                }
                $colType = $valueViewer->getTableColumn()->getType();
                if (!in_array($colType, $allowedColumnTypes, true)) {
                    throw new \UnexpectedValueException(
                        "Column '$columnName' provided for reordering should be of a numeric type (int, float, unix ts)."
                        . "'{$colType}' type is not acceptable'"
                    );
                }
                $valueViewer->setIsSortable(true);
            }
        }
    }

    protected function getSectionTranslationsPrefix($subtype = null) {
        return $subtype === 'value_viewer' ? 'datagrid.column' : 'datagrid';
    }

    /**
     * @param string $parentIdColumnName
     * @param int $limitNestingDepthTo - number of nested data grids. <= 0 - no limit; 1 = 1 subview only;
     * @return $this
     */
    public function enableNestedView($parentIdColumnName = 'parent_id', $limitNestingDepthTo = -1) {
        $this->getTable()->getTableStructure()->getColumn($parentIdColumnName); //< validates column existence
        $this->enableNestedViewBasedOnColumn = $parentIdColumnName;
        $this->setNestedViewsDepthLimit($limitNestingDepthTo);
        $this->setIsRowActionsColumnFixed(false);
        return $this;
    }

    /**
     * @return bool
     */
    public function isNestedViewEnabled() {
        return !empty($this->enableNestedViewBasedOnColumn);
    }

    /**
     * @return null|string
     */
    public function getColumnNameForNestedView() {
        return $this->enableNestedViewBasedOnColumn;
    }

    /**
     * @param int $maxDepth
     * @return $this
     */
    public function setNestedViewsDepthLimit($maxDepth) {
        $this->nestedViewsDepthLimit = (int)$maxDepth;
        return $this;
    }

    /**
     * @return int
     */
    public function getNestedViewsDepthLimit() {
        return $this->nestedViewsDepthLimit;
    }

    /**
     * @param array $rowsPositioningColumns - column that is used to define rows order
     * @return $this
     */
    public function enableRowsReorderingOn(...$rowsPositioningColumns) {
        $this->rowsPositioningColumns = $rowsPositioningColumns;
        return $this;
    }

    /**
     * @return array
     */
    public function getRowsPositioningColumns() {
        return $this->rowsPositioningColumns;
    }

    /**
     * @return bool
     */
    public function isRowsReorderingEnabled() {
        return !empty($this->rowsPositioningColumns);
    }

    /**
     * @return array
     */
    public function getAdditionalViewsForTemplate() {
        return $this->additionalViewsForTemplate instanceof \Closure
            ? call_user_func($this->additionalViewsForTemplate, $this)
            : $this->additionalViewsForTemplate;
    }

    /**
     * Provide additional Laravel views to be inserted after data grid.
     * This will solve almost any problem with complex data grid cells that need to be rendered separately.
     * Use this method in couple with $this->setJsInitiator('jsFunctionName') to have full control over data grid
     * initialization and configuration.
     *
     * Each view will be rendered using view($viewPath, $generalData, $customData) calls.
     *
     * $generalData contains:
     *      'idSuffix' => string
     *      'table' => TableInterface
     *      'dataGridConfig' => DataGridConfig
     * $customData may be provided for each view separately via $views argument
     *
     * @param array|\Closure $views -
     *      - array: list of Laravel views in format
     *          [
     *              'folder.view',
     *              'folder.view2' => $customData,
     *              'ns::folder.view',
     *          ]
     *      - \Closure: function (DataGridConfig $dataGridConfig) { reurn [] } returned value must
     *          fit same format as array (see above)
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setAdditionalViewsForTemplate($views) {
        if (!is_array($views) && !($views instanceof \Closure)) {
            throw new \InvalidArgumentException('$views argument must be an array or \Closure');
        }
        $this->additionalViewsForTemplate = $views;
        return $this;
    }

    /**
     * @return DataGridRendererHelper
     */
    public function getRendererHelper() {
        if (empty($this->rendererHelper)) {
            $this->rendererHelper = new DataGridRendererHelper($this);
        }
        return $this->rendererHelper;
    }

    /**
     * @param array $record
     * @param array $virtualColumns
     * @return array
     * @throws \PeskyCMF\Scaffold\ValueViewerConfigException
     * @throws \PeskyORM\Exception\InvalidDataException
     * @throws \PeskyORM\Exception\OrmException
     */
    public function prepareRecord(array $record, array $virtualColumns = []) {
        $data = parent::prepareRecord($record, $virtualColumns);
        /** @var DataGridColumn $valueViewer */
        foreach ($this->getValueViewers() as $valueViewer) {
            if ($valueViewer->hasRelation()) {
                // add special key for relation column that is not nested into sub object and datatables can find it
                $path = $valueViewer->getRelation()->getName() . '.' . $valueViewer->getRelationColumn();
                $data[$valueViewer::convertNameForDataTables($valueViewer->getName())] = array_get($data, $path);
            }
        }
        return $data;
    }
}


<?php

namespace PeskyCMF\Http\Middleware;

use Illuminate\Http\Request;
use PeskyCMF\Db\HttpRequestStats\CmfHttpRequestStat;
use PeskyORM\Profiling\PeskyOrmPdoProfiler;
use Symfony\Component\HttpFoundation\Response;

class RequestProfiling {

    /**
     * Middleware examples:
     * 1. Use default arguments: \PeskyCMF\Http\Middleware\RequestProfiling::class
     * 2. Disable by default: \PeskyCMF\Http\Middleware\RequestProfiling::class . ':0'
     * 3. Change defaults: \PeskyCMF\Http\Middleware\RequestProfiling::class . ':0,0.5,10'
     *
     * Route examples:
     * 1. Use defaults: Route::get('/path', [ 'profiler' => null ]) or just avoid 'profiler' option
     * 2. Enable using defaults: Route::get('/path', [ 'profiler' => true ])
     * 3. Enable always (ignores limitations): Route::get('/path', [ 'profiler' => 'force' ])
     * 4. Disable: Route::get('/path', [ 'profiler' => false ]) or Route::get('/path', [ 'profiler' => [] ])
     * 5. Enable with custom configs: Route::get('/path', [ 'profiler' => ['min_queries' => 10, 'min_duration' => 0.5] ])
     *
     * How limits work ('min_queries'/$minDbQueries and 'min_duration'/$minDuration):
     * 1. If profiling contains checkpoints - limits are ignored
     * 2. If request duration is more or equals to 'min_duration'/$minDuration or amount of SQL queries is
     * more or equals to 'min_queries'/$minDbQueries - profiling will be saved to db. Otherwise - it won't.
     *
     * Note that profiling slightly affects overall perfomance (~50ms) if enabled so make sure you disable it for
     * requests that do not require profiling anymore.
     *
     * @param Request $request
     * @param \Closure $next
     * @param bool $enabledByDefault
     *  - true - profile all routes until route has 'prifiler' === false option (action):
     *      Route::get('/path', [..., 'profiler' => false, ...]); - profiling for such route will be disabled
     *  - false - profile only routes that have 'prifiler' === true otion (action):
     *      Route::get('/path', [..., 'profiler' => true, ...]); - profiling for such route will be enabled
     * @param float $minDuration - do not record profiling if duration is less then specified
     * @param int $minDbQueries - do not record profiling if min amount of queries is lee then specified
     * @return mixed
     * @throws \LogicException
     * @throws \UnexpectedValueException
     * @throws \PeskyORM\Exception\DbException
     * @throws \PDOException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    public function handle(Request $request, \Closure $next, bool $enabledByDefault = true, float $minDuration = 0, int $minDbQueries = 0) {
        $config = array_get($request->route()->getAction(), 'profiler');
        if ($config === null) {
            $enabled = $enabledByDefault;
            $config = [];
        } else if (empty($config)) {
            $enabled = false;
        } else if (is_array($config)) {
            $enabled = true;
        } else {
            $enabled = true;
            if ($config === 'force') {
                $config = [
                    'min_duration' => 0,
                    'min_queries' => 0,
                ];
            } else {
                $config = [];
            }
        }
        if ($enabled) {
            // begin profiling
            PeskyOrmPdoProfiler::init();
            $stat = CmfHttpRequestStat::createForProfiling();
            // process request
            $response = $next($request);
            // on HTTP response
            if ($response instanceof Response) {
                try {
                    $hasCheckpoints = count($stat->checkpoints_as_array);
                    $stat
                        ->processResponse($response)
                        ->addSqlProfilingData();
                    if (
                        $hasCheckpoints
                        || $stat->duration >= (float)array_get($config, 'min_duration', $minDuration)
                        || (
                            $stat->duration_sql > 0
                            && (int)array_get($stat->sql_as_array, 'statements_count', 999) >= (int)array_get($config, 'min_queries', $minDbQueries)
                        )
                    ) {
                        $stat
                            ->processRequest($request)
                            ->finishAndSave();
                    }
                } catch (\Exception $exception) {
                    $this->logException($exception);
                }
            }
            // save results to DB
            return $response;
        } else {
            return $next($request);
        }
    }

    protected function logException(\Exception $exception, array $context = []) {
        $context['exception'] = $exception;
        \Log::critical($exception->getMessage(), $context);
    }

}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer;

use Ekyna\Component\Commerce\Invoice\Model\InvoiceInterface;
use Ekyna\Component\Resource\Serializer\AbstractResourceNormalizer;

/**
 * Class InvoiceNormalizer
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class InvoiceNormalizer extends AbstractResourceNormalizer
{
    /**
     * @inheritdoc
     *
     * @param InvoiceInterface $invoice
     */
    public function normalize($invoice, $format = null, array $context = [])
    {
        $data = parent::normalize($invoice, $format, $context);

        if ($this->contextHasGroup(['Default', 'OrderInvoice', 'Search'], $context)) {
            $sale = $invoice->getSale();

            $data = array_replace($data, [
                'number'      => $invoice->getNumber(),
                'company'     => $sale->getCompany(),
                'email'       => $sale->getEmail(),
                'first_name'  => $sale->getFirstName(),
                'last_name'   => $sale->getLastName(),
                'type'        => $invoice->getType(),
                'method'      => $invoice->getPaymentMethod()->getName(),
                'description' => $invoice->getDescription(),
                'comment'     => $invoice->getComment(),
            ]);
        } elseif ($this->contextHasGroup(['Summary'], $context)) {
            $items = [];

            foreach ($invoice->getLines() as $item) {
                $items[] = $this->normalizeObject($item, $format, $context);
            }

            $data = array_replace($data, [
                'items'       => $items,
                'description' => $invoice->getDescription(),
                'comment'     => $invoice->getComment(),
            ]);
        }

        return $data;
    }

    /**
     * @inheritdoc
     */
    public function denormalize($data, $class, $format = null, array $context = [])
    {
        //$object = parent::denormalize($data, $class, $format, $context);

        throw new \Exception('Not yet implemented');
    }

    /**
     * @inheritdoc
     */
    public function supportsNormalization($data, $format = null)
    {
        return $data instanceof InvoiceInterface;
    }

    /**
     * @inheritdoc
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        return class_exists($type) && is_subclass_of($type, InvoiceInterface::class);
    }
}


<?php

namespace Ekyna\Component\Commerce\Shipment\Transformer;

use Ekyna\Component\Commerce\Common\Model\CountryInterface;
use Ekyna\Component\Commerce\Common\Model\StateInterface;
use Ekyna\Component\Commerce\Common\Repository\CountryRepositoryInterface;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentAddress;
use libphonenumber\PhoneNumber;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;

/**
 * Class ShipmentAddressTransformer
 * @package Ekyna\Component\Commerce\Shipment\Transformer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class ShipmentAddressTransformer
{
    /**
     * @var CountryRepositoryInterface
     */
    private $countryRepository;

    /**
     * @var PropertyAccessor
     */
    private $accessor;

    /**
     * @var array
     */
    private $fields = [
        'company',
        'gender',
        'firstName',
        'lastName',
        'street',
        'supplement',
        'complement',
        'extra',
        'postalCode',
        'city',
        'country',
        'state',
        'phone',
        'mobile',
        'digicode1',
        'digicode2',
        'intercom',
        'latitude',
        'longitude',
    ];

    /**
     * Constructor.
     *
     * @param CountryRepositoryInterface $countryRepository
     */
    public function __construct(CountryRepositoryInterface $countryRepository)
    {
        $this->countryRepository = $countryRepository;
        $this->accessor = PropertyAccess::createPropertyAccessor();
    }

    /**
     * Returns the country repository.
     *
     * @return CountryRepositoryInterface
     */
    public function getCountryRepository()
    {
        return $this->countryRepository;
    }

    /**
     * Transforms an array into a shipment address.
     *
     * @param array $data
     *
     * @return ShipmentAddress
     */
    public function transform($data)
    {
        $address = new ShipmentAddress();

        if (!is_array($data) || empty($data)) {
            return $address;
        }

        foreach ($this->fields as $field) {
            if (isset($data[$field])) {
                $value = $data[$field];

                if ($field === 'country') {
                    if (0 >= $value) {
                        throw new InvalidArgumentException("Invalid country id.");
                    }
                    $value = $this->countryRepository->find($value);
                    if (null === $value) {
                        throw new InvalidArgumentException("Country not found.");
                    }
                } elseif ($field === 'phone' || $field === 'mobile') {
                    $value = unserialize($value);
                    if (!$value instanceof PhoneNumber) {
                        throw new InvalidArgumentException("Invalid phone number.");
                    }
                }

                $this->accessor->setValue($address, $field, $value);
            }
            // TODO Check required fields ?
        }

        return $address;
    }

    /**
     * Transforms a shipment address in to an array.
     *
     * @param ShipmentAddress $address
     *
     * @return array|null
     */
    public function reverseTransform($address)
    {
        if (null === $address) {
            return null;
        }

        if (!$address instanceof ShipmentAddress) {
            throw new InvalidArgumentException("Expected instance of " . ShipmentAddress::class);
        }

        $data = [];

        foreach ($this->fields as $field) {
            $value = $this->accessor->getValue($address, $field);
            if (empty($value)) {
                continue;
            }

            if ($value instanceof CountryInterface) {
                $value = $value->getId();
            } elseif ($value instanceof StateInterface) {
                $value = $value->getId();
            } elseif ($value instanceof PhoneNumber) {
                $value = serialize($value);
            }

            $data[$field] = $value;
        }

        if (empty($data)) {
            return null;
        }

        return $data;
    }
}


<?php

namespace mii\util;

/**
 * Number helper class. Provides additional formatting methods that for working
 * with numbers.
 *
 * @package    Kohana
 * @category   Helpers
 * @author     Kohana Team
 * @copyright  (c) 2009-2012 Kohana Team
 * @license    http://kohanaframework.org/license
 */
class Num
{

    const ROUND_HALF_UP = 1;
    const ROUND_HALF_DOWN = 2;
    const ROUND_HALF_EVEN = 3;
    const ROUND_HALF_ODD = 4;

    /**
     * @var  array  Valid byte units => power of 2 that defines the unit's size
     */
    public static $byte_units = array
    (
        'B' => 0,
        'K' => 10,
        'Ki' => 10,
        'KB' => 10,
        'KiB' => 10,
        'M' => 20,
        'Mi' => 20,
        'MB' => 20,
        'MiB' => 20,
        'G' => 30,
        'Gi' => 30,
        'GB' => 30,
        'GiB' => 30,
        'T' => 40,
        'Ti' => 40,
        'TB' => 40,
        'TiB' => 40,
        'P' => 50,
        'Pi' => 50,
        'PB' => 50,
        'PiB' => 50,
        'E' => 60,
        'Ei' => 60,
        'EB' => 60,
        'EiB' => 60,
        'Z' => 70,
        'Zi' => 70,
        'ZB' => 70,
        'ZiB' => 70,
        'Y' => 80,
        'Yi' => 80,
        'YB' => 80,
        'YiB' => 80,
    );

    /**
     * Returns the English ordinal suffix (th, st, nd, etc) of a number.
     *
     *     echo 2, Num::ordinal(2);   // "2nd"
     *     echo 10, Num::ordinal(10); // "10th"
     *     echo 33, Num::ordinal(33); // "33rd"
     *
     * @param   integer $number
     * @return  string
     */
    public static function ordinal($number) {
        if ($number % 100 > 10 AND $number % 100 < 14) {
            return 'th';
        }

        switch ($number % 10) {
            case 1:
                return 'st';
            case 2:
                return 'nd';
            case 3:
                return 'rd';
            default:
                return 'th';
        }
    }

    /**
     * Locale-aware number and monetary formatting.
     *
     *     // In English, "1,200.05"
     *     // In Spanish, "1200,05"
     *     // In Portuguese, "1 200,05"
     *     echo Num::format(1200.05, 2);
     *
     *     // In English, "1,200.05"
     *     // In Spanish, "1.200,05"
     *     // In Portuguese, "1.200.05"
     *     echo Num::format(1200.05, 2, TRUE);
     *
     * @param   float $number number to format
     * @param   integer $places decimal places
     * @param   boolean $monetary monetary formatting?
     * @return  string
     * @since   3.0.2
     */
    public static function format($number, $places, $monetary = FALSE) {
        $info = localeconv();

        if ($monetary) {
            $decimal = $info['mon_decimal_point'];
            $thousands = $info['mon_thousands_sep'];
        } else {
            $decimal = $info['decimal_point'];
            $thousands = $info['thousands_sep'];
        }

        return number_format($number, $places, $decimal, $thousands);
    }

    /**
     * Converts a file size number to a byte value. File sizes are defined in
     * the format: SB, where S is the size (1, 8.5, 300, etc.) and B is the
     * byte unit (K, MiB, GB, etc.). All valid byte units are defined in
     * Num::$byte_units
     *
     *     echo Num::bytes('200K');  // 204800
     *     echo Num::bytes('5MiB');  // 5242880
     *     echo Num::bytes('1000');  // 1000
     *     echo Num::bytes('2.5GB'); // 2684354560
     *
     * @param   string $bytes file size in SB format
     * @return  float
     */
    public static function bytes($size) {
        // Prepare the size
        $size = trim((string)$size);

        // Construct an OR list of byte units for the regex
        $accepted = implode('|', array_keys(Num::$byte_units));

        // Construct the regex pattern for verifying the size format
        $pattern = '/^([0-9]+(?:\.[0-9]+)?)(' . $accepted . ')?$/Di';

        // Verify the size format and store the matching parts
        if (!preg_match($pattern, $size, $matches))
            throw new Kohana_Exception('The byte unit size, ":size", is improperly formatted.', array(
                ':size' => $size,
            ));

        // Find the float value of the size
        $size = (float)$matches[1];

        // Find the actual unit, assume B if no unit specified
        $unit = Arr::get($matches, 2, 'B');

        // Convert the size into bytes
        $bytes = $size * pow(2, Num::$byte_units[$unit]);

        return $bytes;
    }

    public static $words = array('комментарий', 'комментария', 'комментариев');

    /**
     * Declination of number
     * @param $number
     * @param mixed $array
     * @return mixed
     */
    public static function decl($number, $array) {

        $cases = array(2, 0, 1, 1, 1, 2);

        if ($number % 100 > 4 AND $number % 100 < 20) {
            return $array[2];
        } else {
            return $array[$cases[min($number % 10, 5)]];
        }
    }

}


<?php

namespace PeskyCMF\Scaffold\ItemDetails;

use PeskyORMLaravel\Db\Column\FilesColumn;
use PeskyORMLaravel\Db\Column\ImagesColumn;
use PeskyORMLaravel\Db\Column\Utils\FileInfo;
use PeskyORMLaravel\Db\Column\Utils\FilesGroupConfig;

/**
 * @method ImagesColumn|FilesColumn getTableColumn()
 */
class ImagesValueCell extends ValueCell {

    /** @var FilesGroupConfig[]|null */
    protected $fileConfigsToShow;
    /** @var string  */
    protected $templateForDefaultRenderer = 'cmf::item_details.images';

    /**
     * List of image names to display.
     * Only provided images will be shown in form. Other images will be ignored (and won't be changed in any way)
     * @param array $imageNames
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setImagesToShow(...$imageNames) {
        if (empty($imageNames)) {
            throw new \InvalidArgumentException('$imageNames argument cannot be empty');
        }
        if (count($imageNames) === 1 && isset($imageNames[0]) && is_array($imageNames[0])) {
            $imageNames = $imageNames[0];
        }
        $this->fileConfigsToShow = $imageNames;
        return $this;
    }

    public function doDefaultValueConversionByType($value, $type, array $record) {
        if (empty($value) || !is_array($value)) {
            return [];
        }
        $column = $this->getTableColumn();
        $object = $this
            ->getScaffoldSectionConfig()
            ->getTable()
            ->newRecord()
            ->enableTrustModeForDbData()
            ->enableReadOnlyMode()
            ->fromData($record, true, false);
        $ret = [];
        $configsToShow = $this->fileConfigsToShow ?: array_keys($this->getTableColumn()->getImagesGroupsConfigurations());
        foreach ($configsToShow as $configName) {
            $preparedInfo = [];
            /** @var FileInfo[] $filesForCofing */
            $filesForCofing = $object->getValue($column, $configName);
            foreach ($filesForCofing as $fileInfo) {
                if ($fileInfo->exists()) {
                    $preparedInfo[] = [
                        'name' => $fileInfo->getFileNameWithExtension(),
                        'original_name' => $fileInfo->getOriginalFileNameWithExtension(),
                        'url' => $fileInfo->getAbsoluteUrl()
                    ];
                }
            }
            if (!empty($preparedInfo)) {
                $ret[] = [
                    'config_name' => $configName,
                    'label' => $this->getScaffoldSectionConfig()->translate($this, $configName),
                    'files' => $preparedInfo
                ];
            }
        }
        return $ret;
    }

}


<?php

namespace Ekyna\Component\Commerce\Common\Model;

use Ekyna\Component\Commerce\Exception\InvalidArgumentException;

/**
 * Class NotificationTypes
 * @package Ekyna\Component\Commerce\Common\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class NotificationTypes
{
    const MANUAL           = 'manual';
    const CART_REMIND      = 'cart_remind';
    const ORDER_ACCEPTED   = 'order_accepted';
    const QUOTE_REMIND     = 'quote_remind';
    const PAYMENT_CAPTURED = 'payment_captured';
    const PAYMENT_EXPIRED  = 'payment_expired';
    const SHIPMENT_SHIPPED = 'shipment_shipped';
    const SHIPMENT_PARTIAL = 'shipment_partial';
    const RETURN_PENDING   = 'return_pending';
    const RETURN_RECEIVED  = 'return_received';

    /**
     * Returns all the types.
     *
     * @return array
     */
    static public function getTypes()
    {
        return [
            static::MANUAL,
            static::CART_REMIND,
            static::ORDER_ACCEPTED,
            static::QUOTE_REMIND,
            static::PAYMENT_CAPTURED,
            static::PAYMENT_EXPIRED,
            static::SHIPMENT_SHIPPED,
            static::SHIPMENT_PARTIAL,
            static::RETURN_PENDING,
            static::RETURN_RECEIVED,
        ];
    }

    /**
     * Returns whether the given type is valid or not.
     *
     * @param string $type
     * @param bool   $throw
     *
     * @return bool
     */
    static public function isValidType($type, $throw = true)
    {
        if (in_array($type, static::getTypes(), true)) {
            return true;
        }

        if ($throw) {
            throw new InvalidArgumentException('Invalid notification type.');
        }

        return false;
    }

    /**
     * Disabled constructor.
     */
    private function __construct()
    {
    }
}


<?php

namespace PeskyCMF\Config;

use Illuminate\Contracts\Support\Arrayable;
use Traversable;

abstract class ConfigsContainer implements \IteratorAggregate, \Countable, Arrayable {

    private $array = null;

    /**
     * Retrieve an external iterator
     * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
     * @return Traversable An instance of an object implementing Iterator or
     * Traversable
     * @since 5.0.0
     */
    public function getIterator() {
        return new \ArrayIterator($this->toArray());
    }

    /**
     * Count elements of an object
     * @link http://php.net/manual/en/countable.count.php
     * @return int The custom count as an integer.
     */
    public function count() {
        return count($this->toArray());
    }

    /**
     * Get the instance as an array.
     * Note 1: it collects returns from all "static public" methods that do not have parameters
     * Collecting done only once. There should be no need to do it more then once.
     * Note 2: array does not contain key "getInstance" but contains key "config_instance" instead
     * @return array
     */
    public function toArray() {
        if ($this->array === null) {
            $this->array = [];
            $reflection = new \ReflectionClass($this);
            /*
             * You may ask why haven't I used \ReflectionMethod::IS_STATIC | \ReflectionMethod::IS_PUBLIC here.
             * The answer is: it is not working in php 5.6.12 or maybe in other versions too
             */
            $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC);
            foreach ($methods as $method) {
                if (
                    $method->isPublic()
                    && $method->getNumberOfParameters() === 0
                    && $method->getName() !== 'getInstance'
                ) {
                    $this->array[$method->getName()] = $method->invoke(null);
                }
            }
            $this->array['config_instance'] = $this;
        }
        return $this->array;
    }
}