src/Admin/Controller/ReserveAdminController.php line 182

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Admin\Controller;
  4. use App\Entity\AdmUser;
  5. use App\Entity\Client;
  6. use App\Entity\Equipment;
  7. use App\Entity\Motorcycle;
  8. use App\Entity\Reserve;
  9. use App\Entity\Type\EquipmentType;
  10. use App\Entity\Type\RentStatusType;
  11. use App\Entity\Type\WeekDay;
  12. use App\EntityService\ReserveEntityService;
  13. use App\Event\ReserveCreateEvent;
  14. use App\Repository\EquipmentRepository;
  15. use App\Repository\MotorcycleRepository;
  16. use App\Repository\ReserveRepository;
  17. use App\Service\TransactionService;
  18. use DateTime;
  19. use Doctrine\ORM\EntityManagerInterface;
  20. use Sonata\AdminBundle\Controller\CRUDController;
  21. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  22. use Symfony\Component\HttpFoundation\JsonResponse;
  23. use Symfony\Component\HttpFoundation\Request;
  24. use Symfony\Component\HttpFoundation\Response;
  25. class ReserveAdminController extends CRUDController
  26. {
  27.     private const DEFAULT_CALENDAR_DAYS_LIMIT  30;
  28.     private EntityManagerInterface $entityManager;
  29.     private ReserveRepository $reserveRepository;
  30.     private MotorcycleRepository $motorcycleRepository;
  31.     private EquipmentRepository $equipmentRepository;
  32.     private ReserveEntityService $reserveEntityService;
  33.     private EventDispatcherInterface $eventDispatcher;
  34.     public function __construct(
  35.         EntityManagerInterface $entityManager,
  36.         ReserveRepository $reserveRepository,
  37.         MotorcycleRepository $motorcycleRepository,
  38.         EquipmentRepository $equipmentRepository,
  39.         ReserveEntityService $reserveEntityService,
  40.         EventDispatcherInterface $eventDispatcher
  41.     )
  42.     {
  43.         $this->entityManager $entityManager;
  44.         $this->reserveRepository $reserveRepository;
  45.         $this->motorcycleRepository $motorcycleRepository;
  46.         $this->equipmentRepository $equipmentRepository;
  47.         $this->reserveEntityService $reserveEntityService;
  48.         $this->eventDispatcher $eventDispatcher;
  49.     }
  50.     public function getReserveCalendarAction(Request $request): JsonResponse
  51.     {
  52.         $days $request->get('days');
  53.         if (!$days) {
  54.             $days self::DEFAULT_CALENDAR_DAYS_LIMIT;
  55.         }
  56.         $searchStartDate = new \DateTime();
  57.         $searchEndDate = new \DateTime();
  58.         $searchEndDate->setTime(235959);
  59.         $searchStartDate->modify('- 1 day');
  60.         $searchEndDate->modify('- 1 day');
  61.         $graph = [];
  62.         for ($i 0$i <= $days$i++) {
  63.             $searchStartDate->setTime(0,0);
  64.             $searchStartDate->modify('+ 1 day');
  65.             $searchEndDate->modify('+ 1 day');
  66.             $result $this->reserveRepository->reserveList($searchStartDate$searchEndDate);
  67.             $reserves = [];
  68.             $result->map(static function (Reserve $reserve) use (&$reserves$searchStartDate$searchEndDate) {
  69.                 $motId $reserve->getMotorcycle()->getId();
  70.                 $motName $reserve->getMotorcycle()->getFullName();
  71.                 $startTime $reserve->getStartDate();
  72.                 
  73.                 $endDate $reserve->getEndDate();
  74.                 $endDay = (clone $searchEndDate)->setTime(23,59);
  75.                 if ((int)$startTime->format('d') !== (int) $searchStartDate->format('d')) {
  76.                     $startTime = clone $searchStartDate;
  77.                 }
  78.                 if ($endDate $endDay) {
  79.                     $endDate $endDay;
  80.                 }
  81.                 // Normalize to full hours for the calendar grid.
  82.                 // A rental starting at 15:15 will occupy the 15:00 slot onward on this day.
  83.                 $slotStart = (clone $startTime)->setTime((int)$startTime->format('H'), 00);
  84.                 $list = [];
  85.                 $current = clone $slotStart;
  86.                 while ($current <= $endDate) {
  87.                     $list[] = $current->format('H:i');
  88.                     $current->modify('+ 1 hour');
  89.                 }
  90.                 if ((int)$reserve->getEndDate()->format('d') !== (int) $searchEndDate->format('d') ||
  91.                     (int)$reserve->getEndDate()->format('m') > (int) $searchEndDate->format('m')
  92.                 ) {
  93.                     $list[] = '24:00';
  94.                 }
  95.                 
  96.                 // Removed array_pop so the last slot (end hour) is included for coloring and red border on end day.
  97.                 // For exact hour end on end day, the last in list will be the end hour, full color + red on its right edge.
  98.                 if (!isset($reserves[$motId])) {
  99.                     $reserves[$motId] = [
  100.                         'name' => $motName,
  101.                         'color' => $reserve->getMotorcycle()->getColor(),
  102.                         'segments' => []
  103.                     ];
  104.                 }
  105.                 $starts_today = ($reserve->getStartDate()->format('Y-m-d') === $searchStartDate->format('Y-m-d'));
  106.                 $ends_today = ($reserve->getEndDate()->format('Y-m-d') === $searchStartDate->format('Y-m-d'));
  107.                 $reserves[$motId]['segments'][] = [
  108.                     'times' => $list,
  109.                     'starts_today' => $starts_today,
  110.                     'ends_today' => $ends_today,
  111.                     'real_start' => $reserve->getStartDate()->format('H:i'),
  112.                     'real_end'   => $reserve->getEndDate()->format('H:i'),
  113.                 ];
  114.             });
  115.             $schedules $this->prepareReserves($reserves);
  116.             $graph[] = [
  117.                 'date' => sprintf(
  118.                     '(%s) %s',
  119.                     WeekDay::WEEKDAY_RU[(int)$searchStartDate->format('w')],
  120.                     $searchStartDate->format('d.m.Y')),
  121.                 'schedules' => $schedules
  122.             ];
  123.         }
  124.         return new JsonResponse($graph);
  125.     }
  126.     private function prepareReserves($reserves): array
  127.     {
  128.         $schedules = [];
  129.         foreach ($reserves as $motId => $value) {
  130.             $schedules[] = [
  131.                 'id' => $motId,
  132.                 'name' => $value['name'],
  133.                 'color' => $value['color'],
  134.                 'segments' => $value['segments']
  135.             ];
  136.         }
  137.         return $schedules;
  138.     }
  139.     public function createAcceptanceCertificateAction(Request $request): Response
  140.     {
  141.         /** @var Reserve $reserve */
  142.         $reserve $this->reserveRepository->find($request->get('id'));
  143.         $this->reserveEntityService->toActiveStatus($reserve);
  144.         $this->entityManager->flush();
  145.         $this->eventDispatcher->dispatch(new ReserveCreateEvent($reserve), ReserveCreateEvent::NAME);
  146.         $data $this->getAcceptanceCertificateViewData($reserve);
  147.         return $this->renderWithExtraParams('admin/Document/acceptance_certificate.html.twig'$data);
  148.     }
  149.     /**
  150.      * Simple view of the acceptance certificate (акт) without any side effects.
  151.      * Used from the "Просмотреть акт" button on active reserves list.
  152.      */
  153.     public function viewAcceptanceCertificateAction(Request $request): Response
  154.     {
  155.         $reserve $this->reserveRepository->find($request->get('id'));
  156.         if (!$reserve) {
  157.             throw $this->createNotFoundException('Reserve not found');
  158.         }
  159.         $data $this->getAcceptanceCertificateViewData($reserve);
  160.         return $this->renderWithExtraParams('admin/Document/acceptance_certificate.html.twig'$data);
  161.     }
  162.     /**
  163.      * Prepares the data array for the acceptance_certificate.html.twig template.
  164.      * Shared between create (which does activation) and pure view.
  165.      */
  166.     private function getAcceptanceCertificateViewData(Reserve $reserve): array
  167.     {
  168.         /** @var AdmUser $admin */
  169.         $admin $this->getUser();
  170.         /** @var Client $client */
  171.         $client $reserve->getClient();
  172.         /** @var Motorcycle $motorcycle */
  173.         $motorcycle $reserve->getMotorcycle();
  174.         $equipment = [];
  175.         /** @var Equipment $eq */
  176.         foreach ($reserve->getEquipment() as $eq) {
  177.             $number ' (№ ' $eq->getSerialNumber() . ')';
  178.             $equipment[] = EquipmentType::RU_VALUES[$eq->getType()] . ' ' $eq->getBrand() . ' ' $eq->getModel() . ' ' $eq->getSize() . $number ;
  179.         }
  180.         return [
  181.             'admin' => [
  182.                 'jobTitle' => $admin $admin->getBiography() : '',
  183.                 'name' => $admin ? ($admin->getLastname() . ' ' .  $admin->getFirstname()) : ''
  184.             ],
  185.             'currentDate' => (new DateTime())->format('d.m.Y'),
  186.             'client' => [
  187.                 'number' => $client $client->getUserNumber() : '',
  188.                 'fio' => $client $client->getFio() : '',
  189.                 'signDate' => ($client && $client->getDateSigningContract()) ? $client->getDateSigningContract()->format('d.m.Y') : ''
  190.             ],
  191.             'motorcycle' => [
  192.                 'name' => $motorcycle $motorcycle->getFullName() : '',
  193.                 'vin' => $motorcycle $motorcycle->getVinNumber() : '',
  194.                 'year' => $motorcycle && $motorcycle->getIssueYear() ? $motorcycle->getIssueYear()->format('Y') : '',
  195.                 'registrationNumber' => $motorcycle $motorcycle->getRegistrationNumber() : '',
  196.                 'paymentAmount' => $motorcycle $motorcycle->getPaymentAmount() : null,
  197.                 'mileage' => $motorcycle $motorcycle->getMileage() : null,
  198.             ],
  199.             'reserve' => [
  200.                 'amount' => $reserve->getAmount(),
  201.                 'amountWords' => $this->num2str($reserve->getAmount()),
  202.                 'startDate' => $reserve->getStartDate()->format('d.m.Y H:i'),
  203.                 'endDate' => $reserve->getEndDate()->format('d.m.Y H:i'),
  204.                 'returnDate' => $reserve->getEndDate()->format('d.m.Y'),
  205.                 'returnTimeIntervalStart' => $reserve->getEndDate()->format('H:i'),
  206.                 'returnTimeIntervalEnd' => (clone $reserve->getEndDate())->modify('+ 15 minutes')->format('H:i'),
  207.             ],
  208.             'equipment' => $equipment
  209.         ];
  210.     }
  211.     public function openReserveAction(Request $request): Response
  212.     {
  213.         $reserve $this->reserveRepository->find($request->get('id'));
  214.         ///На потом, достать весь экип у которого нет активной аренды (или типы аренды котрые раньше)
  215.         /// т текущий экип если уже брали
  216.         return $this->renderWithExtraParams('admin/Reserve/open_reserve_page.html.twig', [
  217.             'object' => $reserve,
  218.             'equipment' => $this->getEquipmentPrepare(),
  219.             'jsonEquipment' => json_encode($this->getEquipmentPrepare())
  220.         ]);
  221.     }
  222.     public function closeReserveAction(Request $request): Response
  223.     {
  224.         $reserve $this->reserveRepository->find($request->get('id'));
  225.         return $this->renderWithExtraParams('admin/Reserve/close_reserve_page.html.twig', [
  226.             'object' => $reserve,
  227.         ]);
  228.     }
  229.     public function finaleCloseReserveAction(Request $request): Response
  230.     {
  231.         /** @var Reserve $reserve */
  232.         $reserve $this->reserveRepository->find($request->get('id'));
  233.         if (!$reserve) {
  234.             throw $this->createNotFoundException('Reserve not found');
  235.         }
  236.         $payload $this->extractRequestPayload($request);
  237.         $endMileage $payload['endMileage'] ?? $request->get('endMileage') ?? $request->get('mileage');
  238.         if ($endMileage !== null && $endMileage !== '') {
  239.             $mileage = (int) $endMileage;
  240.             $reserve->setEndRentMileage($mileage);
  241.             if ($reserve->getMotorcycle()) {
  242.                 $reserve->getMotorcycle()->setMileage($mileage);
  243.             }
  244.         }
  245.         $reserve->setStatus(RentStatusType::ENDED);
  246.         $this->entityManager->flush();
  247.         return $this->redirectToRoute('admin_app_reserve_active_list');
  248.     }
  249.     public function addToReserveAction(Request $request): Response
  250.     {
  251.         try {
  252.             $payload $this->extractRequestPayload($request);
  253.             $reserveId $payload['reserve'] ?? $request->get('reserve');
  254.             $equipmentList $payload['equipment'] ?? $request->get('equipment');
  255.             $startReserveMileage $payload['startMileage'] ?? $request->get('startMileage');
  256.             $endReserveMileage $payload['endMileage'] ?? $request->get('endMileage');
  257.             /** @var Reserve|null $reserve */
  258.             $reserve $this->reserveRepository->find((int) $reserveId);
  259.             if (!$reserve) {
  260.                 return new JsonResponse('Reserve not found'Response::HTTP_NOT_FOUND);
  261.             }
  262.             if ($startReserveMileage !== null && $startReserveMileage !== '') {
  263.                 $mileage = (int) $startReserveMileage;
  264.                 $reserve->setStartRentMileage($mileage);
  265.                 if ($reserve->getMotorcycle()) {
  266.                     $reserve->getMotorcycle()->setMileage($mileage);
  267.                 }
  268.             }
  269.             if ($endReserveMileage !== null && $endReserveMileage !== '') {
  270.                 $mileage = (int) $endReserveMileage;
  271.                 $reserve->setEndRentMileage($mileage);
  272.                 if ($reserve->getMotorcycle()) {
  273.                     $reserve->getMotorcycle()->setMileage($mileage);
  274.                 }
  275.             }
  276.             if (is_array($equipmentList)) {
  277.                 foreach ($equipmentList as $item) {
  278.                     /** @var Equipment|null $equipment */
  279.                     $equipment $this->equipmentRepository->find((int) $item);
  280.                     if ($equipment) {
  281.                         $reserve->addEquipment($equipment);
  282.                     }
  283.                 }
  284.             }
  285.             $this->entityManager->flush();
  286.             return new JsonResponse(['success' => true]);
  287.         } catch (\Throwable $e) {
  288.             return new JsonResponse($e->getMessage(), Response::HTTP_I_AM_A_TEAPOT);
  289.         }
  290.     }
  291.     /**
  292.      * Supports both JSON body (fetch) and classic form/query params.
  293.      */
  294.     private function extractRequestPayload(Request $request): array
  295.     {
  296.         $content $request->getContent();
  297.         if ($content) {
  298.             $decoded json_decode($contenttrue);
  299.             if (is_array($decoded)) {
  300.                 return $decoded;
  301.             }
  302.         }
  303.         return $request->request->all();
  304.     }
  305.     /**
  306.      * AJAX action to calculate reserve amount.
  307.      * Now supports dynamic calculation: breaks down the total rental time
  308.      * into combination of available packages (сутки + 6 часов etc.) and sums their prices.
  309.      * Example: 24h (160) + 6h (90) = 250.
  310.      */
  311.     public function calculateAmountAction(Request $request): JsonResponse
  312.     {
  313.         $motorcycleId $request->request->get('motorcycle');
  314.         $startStr $request->request->get('start');
  315.         $endStr $request->request->get('end');
  316.         $durationKey $request->request->get('duration');
  317.         if (!$motorcycleId || !$startStr) {
  318.             return new JsonResponse(['success' => false'error' => 'Не указан мотоцикл или дата начала'], 400);
  319.         }
  320.         /** @var \App\Entity\Motorcycle|null $motorcycle */
  321.         $motorcycle $this->motorcycleRepository->find((int)$motorcycleId);
  322.         if (!$motorcycle) {
  323.             return new JsonResponse(['success' => false'error' => 'Мотоцикл не найден'], 404);
  324.         }
  325.         $rentalPrice $motorcycle->getRentalPrice();
  326.         if (!$rentalPrice) {
  327.             return new JsonResponse([
  328.                 'success' => false
  329.                 'error' => 'У этого мотоцикла не назначен профиль цен аренды. Укажите профиль в карточке мотоцикла.'
  330.             ], 400);
  331.         }
  332.         try {
  333.             $start = new \DateTime($startStr);
  334.         } catch (\Exception $e) {
  335.             return new JsonResponse(['success' => false'error' => 'Некорректная дата начала'], 400);
  336.         }
  337.         // Prefer dynamic calculation when both start and end are available (this is the new main path)
  338.         if ($endStr) {
  339.             try {
  340.                 $end = new \DateTime($endStr);
  341.                 if ($end <= $start) {
  342.                     return new JsonResponse(['success' => false'error' => 'Дата окончания должна быть позже даты начала'], 400);
  343.                 }
  344.                 $diffSeconds $end->getTimestamp() - $start->getTimestamp();
  345.                 $totalHours $diffSeconds 3600;
  346.                 $calc $this->calculateDynamicPrice($rentalPrice$start$totalHours);
  347.                 return new JsonResponse([
  348.                     'success' => true,
  349.                     'amount' => $calc['amount'],
  350.                     'breakdown' => $calc['breakdown'],
  351.                     'total_hours' => round($totalHours2),
  352.                     'profile' => (string)$rentalPrice,
  353.                 ]);
  354.             } catch (\Exception $e) {
  355.                 return new JsonResponse(['success' => false'error' => 'Ошибка при расчёте по датам'], 400);
  356.             }
  357.         }
  358.         // Fallback: single package by durationKey (old behavior, useful if only quick button used without end date yet)
  359.         if ($durationKey) {
  360.             if ($durationKey === '5h') {
  361.                 $weekday = (int)$start->format('N');
  362.                 $isWeekday $weekday <= 5;
  363.                 $startHour = (int)$start->format('H');
  364.                 if (!$isWeekday || $startHour 18) {
  365.                     return new JsonResponse([
  366.                         'success' => false,
  367.                         'error' => 'Пакет «5 часов (вечер)» доступен только по будням (пн–пт) и только если аренда начинается в 18:00 или позже.'
  368.                     ], 400);
  369.                 }
  370.             }
  371.             $amount $rentalPrice->getPriceForDuration($durationKey);
  372.             if ($amount === null || $amount <= 0) {
  373.                 return new JsonResponse([
  374.                     'success' => false
  375.                     'error' => 'Для выбранной длительности не задана цена в профиле.'
  376.                 ], 400);
  377.             }
  378.             return new JsonResponse([
  379.                 'success' => true,
  380.                 'amount' => $amount,
  381.                 'breakdown' => $durationKey ' (по профилю ' $rentalPrice ')',
  382.                 'profile' => (string)$rentalPrice,
  383.             ]);
  384.         }
  385.         return new JsonResponse(['success' => false'error' => 'Укажите даты окончания или выберите длительность для расчёта.'], 400);
  386.     }
  387.     /**
  388.      * Dynamic price calculation.
  389.      * Breaks total rental time into combination of available packages and sums the prices.
  390.      * Supports combinations like "сутки + 6 часов".
  391.      */
  392.     private function calculateDynamicPrice(\App\Entity\RentalPrice $rentalPrice, \DateTimeInterface $startfloat $totalHours): array
  393.     {
  394.         // Packages in descending order of duration
  395.         $packages = [
  396.             ['key' => '30d''hours' => 720'price' => $rentalPrice->getPrice30d(), 'label' => '30 дней'],
  397.             ['key' => '7d',  'hours' => 168'price' => $rentalPrice->getPrice7d(),  'label' => 'неделя'],
  398.             ['key' => '24h''hours' => 24,  'price' => $rentalPrice->getPrice24h(), 'label' => 'сутки'],
  399.             ['key' => '6h',  'hours' => 6,   'price' => $rentalPrice->getPrice6h(),  'label' => '6 часов'],
  400.             ['key' => '5h',  'hours' => 5,   'price' => $rentalPrice->getPrice5h(),  'label' => '5 часов (вечер)'],
  401.             ['key' => '1h',  'hours' => 1,   'price' => $rentalPrice->getPrice1h(),  'label' => '1 час'],
  402.         ];
  403.         $remaining $totalHours;
  404.         $parts = [];
  405.         $total 0.0;
  406.         $canUse5h null// cached check for the special package
  407.         foreach ($packages as $pkg) {
  408.             if ($pkg['price'] === null || $pkg['price'] <= || $pkg['hours'] <= 0) {
  409.                 continue;
  410.             }
  411.             $count = (int) floor($remaining $pkg['hours']);
  412.             if ($count 0) {
  413.                 if ($pkg['key'] === '5h') {
  414.                     if ($canUse5h === null) {
  415.                         $weekday = (int)$start->format('N'); // 1=Mon ... 5=Fri
  416.                         $startHour = (int)$start->format('H');
  417.                         $canUse5h = ($weekday <= && $startHour >= 18);
  418.                     }
  419.                     if (!$canUse5h) {
  420.                         continue; // skip 5h package, fall back to smaller units
  421.                     }
  422.                 }
  423.                 $parts[] = $count ' × ' $pkg['label'] . ' (' $pkg['price'] . ')';
  424.                 $total += $count $pkg['price'];
  425.                 $remaining -= $count $pkg['hours'];
  426.             }
  427.         }
  428.         // Handle any small remaining time using 1h price (round up)
  429.         if ($remaining 0.01) {
  430.             $oneHPrice $rentalPrice->getPrice1h();
  431.             if ($oneHPrice 0) {
  432.                 $extra = (int) ceil($remaining);
  433.                 $parts[] = $extra ' × 1 час (' $oneHPrice ')';
  434.                 $total += $extra $oneHPrice;
  435.             }
  436.         }
  437.         $breakdown implode(' + '$parts);
  438.         if ($breakdown) {
  439.             $breakdown .= ' = ' $total;
  440.         } else {
  441.             $breakdown 'Не удалось подобрать пакеты для указанного времени';
  442.         }
  443.         return [
  444.             'amount' => $total,
  445.             'breakdown' => $breakdown,
  446.         ];
  447.     }
  448.     private function getEquipmentPrepare(): array
  449.     {
  450.         $equipment $this->equipmentRepository->findAll();
  451.         $res = [];
  452.         foreach ($equipment as $item) {
  453.             $image $item->getImages()->first();
  454.             $res[EquipmentType::VALUES[$item->getType()]][] = [
  455.                 'id' => $item->getId(),
  456.                 'brand' => $item->getBrand(),
  457.                 'model' => $item->getModel(),
  458.                 'size' => $item->getSize(),
  459.                 'image' => $image $image->getPath() : null
  460.             ];
  461.         }
  462.         return $res;
  463.     }
  464.     private function num2str($num): string
  465.     {
  466.         $nul='ноль';
  467.         $ten=array(
  468.             array('','один','два','три','четыре','пять','шесть','семь''восемь','девять'),
  469.             array('','одна','две','три','четыре','пять','шесть','семь''восемь','девять'),
  470.         );
  471.         $a20=array('десять','одиннадцать','двенадцать','тринадцать','четырнадцать' ,'пятнадцать','шестнадцать','семнадцать','восемнадцать','девятнадцать');
  472.         $tens=array(2=>'двадцать','тридцать','сорок','пятьдесят','шестьдесят','семьдесят' ,'восемьдесят','девяносто');
  473.         $hundred=array('','сто','двести','триста','четыреста','пятьсот','шестьсот''семьсот','восемьсот','девятьсот');
  474.         $unit=array( // Units
  475.             array('копейка' ,'копейки' ,'копеек',     1),
  476.             array('рубль'   ,'рубля'   ,'рублей'    ,0),
  477.             array('тысяча'  ,'тысячи'  ,'тысяч'     ,1),
  478.             array('миллион' ,'миллиона','миллионов' ,0),
  479.             array('миллиард','милиарда','миллиардов',0),
  480.         );
  481.         //
  482.         list($rub,$kop) = explode('.',sprintf("%015.2f"floatval($num)));
  483.         $out = array();
  484.         if (intval($rub)>0) {
  485.             foreach(str_split($rub,3) as $uk=>$v) { // by 3 symbols
  486.                 if (!intval($v)) continue;
  487.                 $uk sizeof($unit)-$uk-1// unit key
  488.                 $gender $unit[$uk][3];
  489.                 list($i1,$i2,$i3) = array_map('intval',str_split($v,1));
  490.                 // mega-logic
  491.                 $out[] = $hundred[$i1]; # 1xx-9xx
  492.                 if ($i2>1$out[]= $tens[$i2].' '.$ten[$gender][$i3]; # 20-99
  493.                 else $out[]= $i2>$a20[$i3] : $ten[$gender][$i3]; # 10-19 | 1-9
  494.                 // units without rub & kop
  495.                 if ($uk>1$out[]= $this->morph($v,$unit[$uk][0],$unit[$uk][1],$unit[$uk][2]);
  496.             } //foreach
  497.         }
  498.         else $out[] = $nul;
  499. //        $out[] = $this->morph(intval($rub), $unit[1][0],$unit[1][1],$unit[1][2]); // rub
  500. //        $out[] = $kop.' '.$this->morph($kop,$unit[0][0],$unit[0][1],$unit[0][2]); // kop
  501.         return trim(preg_replace('/ {2,}/'' 'join(' ',$out)));
  502.     }
  503.     private function morph($n$f1$f2$f5) {
  504.         $n abs(intval($n)) % 100;
  505.         if ($n>10 && $n<20) return $f5;
  506.         $n $n 10;
  507.         if ($n>&& $n<5) return $f2;
  508.         if ($n==1) return $f1;
  509.         return $f5;
  510.     }
  511. }