Skip to content

Commit

Permalink
[shopsys] added ability to schedule each cron for specific time same …
Browse files Browse the repository at this point in the history
…way as crons (#2922)
  • Loading branch information
TomasLudvik committed Nov 28, 2023
2 parents 635e01b + 7c6469c commit df87fcd
Show file tree
Hide file tree
Showing 42 changed files with 852 additions and 231 deletions.
68 changes: 68 additions & 0 deletions UPGRADE-14.0.md
Expand Up @@ -141,6 +141,74 @@ Follow the instructions in relevant sections, e.g. `shopsys/coding-standards` or
- see #project-base-diff to update your project
- prevent duplicate color parameters in data fixtures ([#2911](https://github.com/shopsys/shopsys/pull/2911))
- see #project-base-diff to update your project
- enable crons to be run at specified times the same as crons ()
- replace `HourlyFeedCronModule` and `DailyFeedCronModule` with `FeedCronModule` in `config/services/cron.yaml` and set it to be run every time crons are run to ensure that all feeds are generated
- `FeedExportCreationDataQueue` has changed, first parameter is now an array of `Shopsys\FrameworkBundle\Model\Feed\FeedModule` instances instead of module names
- method `Shopsys\FrameworkBundle\Model\Feed\FeedFacade::__construct()` changed its interface:
```diff
public function __construct(
protected readonly FeedRegistry $feedRegistry,
protected readonly ProductVisibilityFacade $productVisibilityFacade,
protected readonly FeedExportFactory $feedExportFactory,
protected readonly FeedPathProvider $feedPathProvider,
protected readonly FilesystemOperator $filesystem,
+ protected readonly FeedModuleRepository $feedModuleRepository,
+ protected readonly EntityManagerInterface $em,
)
```
- method `Shopsys\FrameworkBundle\Model\Feed\FeedFacade::getFeedsInfo()` changed its interface:
```diff
- public function getFeedsInfo(?string $feedType = null): array
+ public function getFeedsInfo(bool $onlyForCurrentTime = false): array
```
- method `Shopsys\FrameworkBundle\Model\Feed\FeedFacade::getFeedsNames()` changed its interface:
```diff
- public function getFeedNames(?string $feedType = null): array
+ public function getFeedNames(bool $onlyForCurrentTime = false): array
```
- method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::__construct()` changed its interface:
```diff
public function __construct(
- protected readonly array $knownTypes,
- protected readonly string $defaultType,
+ protected readonly FeedRegistry $feedRegistry,
+ protected readonly ProductVisibilityFacade $productVisibilityFacade,
+ protected readonly FeedExportFactory $feedExportFactory,
+ protected readonly FeedPathProvider $feedPathProvider,
+ protected readonly FilesystemOperator $filesystem,
+ protected readonly FeedModuleRepository $feedModuleRepository,
+ protected readonly EntityManagerInterface $em,
)
```
- method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::registerFeed()` changed its interface:
```diff
- public function registerFeed(FeedInterface $feed, ?string $type = null): void
+ public function registerFeed(FeedInterface $feed, string $timeHours, string $timeMinutes, array $domainIds): void
```
- property `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::$feedsByType` has been replaced with new `$feedConfigsByName` property instead
- method `Shopsys\FrameworkBundle\Controller\Admin\FeedController::__construct` changed its interface:
```diff
public function __construct(
protected readonly FeedFacade $feedFacade,
protected readonly GridFactory $gridFactory,
protected readonly Domain $domain,
+ protected readonly FeedRegistry $feedRegistry,
+ protected readonly FeedModuleRepository $feedModuleRepository,
)
```
- method `Shopsys\FrameworkBundle\Model\Feed\Exception\FeedNotFoundException__construct` changed its interface:
```diff
public function __construct(
string $name,
+ ?int $domainId = null,
?Exception $previous = null
)
```
- method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::getFeeds()` has been replaced with method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::getFeedsForCurrentTime()`
- method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::getAllFeeds()` has been replaced with method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::getAllFeedConfigs()`
- method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::getFeedByName()` has been replaced with method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::getFeedConfigByName()`
- method `Shopsys\FrameworkBundle\Model\Feed\FeedRegistry::assertTypeIsKnown()` has been removed without a replacement
- see #project-base-diff to update your project

### Storefront

Expand Down
3 changes: 1 addition & 2 deletions docs/cookbook/basic-data-import.md
Expand Up @@ -467,8 +467,7 @@ You will get a list of all available cron modules as an output.
```text
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Component\Error\ErrorPageCronModule"
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Cart\Item\DeleteOldCartsCronModule"
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Feed\DailyFeedCronModule"
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Feed\HourlyFeedCronModule"
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Feed\FeedCronModule"
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Pricing\Vat\VatDeletionCronModule"
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Product\Availability\ProductAvailabilityCronModule"
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Product\Pricing\ProductPriceCronModule"
Expand Down
3 changes: 1 addition & 2 deletions docs/cookbook/working-with-multiple-cron-instances.md
Expand Up @@ -49,8 +49,7 @@ default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Component\Error\ErrorPageCronModule" --instance-name=default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Cart\Item\DeleteOldCartsCronModule" --instance-name=default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Feed\DailyFeedCronModule" --instance-name=default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Feed\HourlyFeedCronModule" --instance-name=default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Feed\FeedCronModule" --instance-name=default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Pricing\Vat\VatDeletionCronModule" --instance-name=default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Product\Availability\ProductAvailabilityCronModule" --instance-name=default
php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Product\Pricing\ProductPriceCronModule" --instance-name=default
Expand Down
51 changes: 42 additions & 9 deletions docs/model/product-feeds.md
Expand Up @@ -2,7 +2,7 @@

Product feeds are a way to periodically export information about your products for product search engines such as [Google Shopping](https://www.google.com/shopping).

In order to allow easy installation and removal of product feeds, they are implemented in form of plugins ([see list of current implementations](https://github.com/search?q=topic%3Aproduct-feed+org%3Ashopsys)).
To allow easy installation and removal of product feeds, they are implemented in form of plugins ([see list of current implementations](https://github.com/search?q=topic%3Aproduct-feed+org%3Ashopsys)).

## Where are the feeds?

Expand All @@ -12,22 +12,55 @@ You can see all installed product feeds along with the URLs of their export in t
## When are they exported?

Product feeds are usually exported using Cron modules.
The Cron modules are already implemented and registered, all that's needed is to run the [`cron` phing target](../introduction/console-commands-for-application-management-phing-targets.md#cron) every 5 minutes on your server and Shopsys Platform takes care of the rest.
The Cron modules are already implemented and registered, all that's needed is to run the [`cron` phing target](../introduction/console-commands-for-application-management-phing-targets.md#cron) every 5 minutes (can be changed in parameters) on your server and Shopsys Platform takes care of the rest.
They can be also generated manually in the administration section _Marketing > XML Feeds_, if you're logged in as _superadministrator_.

There are two types of product feeds: `daily` and `hourly`.
Each feed definition in `services.yaml` includes hours and minutes in cron format, when it should be generated.

Daily feed usually contain a lot of information about the products are can take a while to export if you have a lot of products.
For this exact reason, the [`DailyFeedCronModule`](https://github.com/shopsys/shopsys/blob/master/packages/framework/src/Model/Feed/DailyFeedCronModule.php) is implemented iteratively and can export the feeds in batches as needed.
For example:

Hourly feeds contain much less information and their priority is to be as current as possible.
Typical examples are exports of current availability and stock quantities of the products.
By default, they are exported every hour and the export cannot be broken into multiple Cron executions.
```yaml
Shopsys\ProductFeed\GoogleBundle\GoogleFeed:
tags:
- { name: shopsys.product_feed, hours: '1', minutes: '0' }
```

Google feed will be generated every day at 1:00 AM.

You can also set it like this:

```yaml
Shopsys\ProductFeed\GoogleBundle\GoogleFeed:
tags:
- { name: shopsys.product_feed, hours: '*/4', minutes: '0' }
```

In such a case, this feed will be generated every four hours.

Feeds have their default times set in their own `services.yaml` files, but can be easily changed in your project's `feed.yaml` file.
You only need to copy service definition from feed `services.yaml` file and change the hours and minutes to expected ones.

## How to run feed outside scheduled time?

If you want to run feed outside scheduled time, you can use the `php bin/console shopsys:feed-schedule` command with `--feed-name` argument to schedule one feed or `--all` argument to schedule all feeds and then run `php bin/console shopsys:cron --module="Shopsys\FrameworkBundle\Model\Feed\FeedCronModule" --instance-name=export` to generate the feeds.

## How to limit a product feed to a specific domain?

If you want to limit a product feed to a specific domain, you can use the `domain_ids` parameter in the feed's service definition.
Copy the service definition from the feed's `services.yaml` file and add the `domain_ids` parameter with the IDs of the domains you want to limit the feed to in your projects `feed.yaml` file.

For example, if you want to limit Google Feed to first and third domain, you will add this to your `feed.yaml` file.

```yaml
Shopsys\ProductFeed\GoogleBundle\GoogleFeed:
tags:
- { name: shopsys.product_feed, hours: '1', minutes: '0', domain_ids: '1,3' }
```

## How to implement a custom product feed?

The heart of a product feed plugin is a service implementing the [`FeedInterface`](https://github.com/shopsys/shopsys/blob/master/packages/framework/src/Model/Feed/FeedInterface.php) that is tagged in a DI container with `shopsys.product_feed` tag.
Optionally, the tag can have a type attribute (default is `daily`).
Tags `hours` and `minutes` are mandatory and define when the feed should be generated.

The annotations in the feed interfaces ([`FeedInterface`](https://github.com/shopsys/shopsys/blob/master/packages/framework/src/Model/Feed/FeedInterface.php), [`FeedInfoInterface`](https://github.com/shopsys/shopsys/blob/master/packages/framework/src/Model/Feed/FeedInfoInterface.php) and [`FeedItemInterface`](https://github.com/shopsys/shopsys/blob/master/packages/framework/src/Model/Feed/FeedItemInterface.php)) should explain a lot.
When in doubt, you can take a look at the [already implemented product feeds](https://github.com/search?q=topic%3Aproduct-feed+org%3Ashopsys) for inspiration.
Expand Down
18 changes: 2 additions & 16 deletions packages/framework/src/Command/CronCommand.php
Expand Up @@ -4,14 +4,13 @@

namespace Shopsys\FrameworkBundle\Command;

use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use NinjaMutex\Lock\LockInterface;
use Shopsys\FrameworkBundle\Command\Exception\CronCommandException;
use Shopsys\FrameworkBundle\Component\Cron\Config\CronModuleConfig;
use Shopsys\FrameworkBundle\Component\Cron\CronFacade;
use Shopsys\FrameworkBundle\Component\Cron\MutexFactory;
use Shopsys\FrameworkBundle\Component\DateTimeHelper\DateTimeHelper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -182,7 +181,7 @@ private function runCron(
}

if ($runAllModules) {
$cronFacade->scheduleModulesByTime($this->getCurrentRoundedTime($instanceRunEveryMin));
$cronFacade->scheduleModulesByTime(DateTimeHelper::getCurrentRoundedTimeForIntervalAndTimezone($instanceRunEveryMin, $this->getCronTimeZone()));
}

$mutex = $mutexFactory->getPrefixedCronMutex($instanceName);
Expand All @@ -201,19 +200,6 @@ private function runCron(
$mutex->releaseLock();
}

/**
* @param int $runEveryMin
* @return \DateTimeImmutable
*/
private function getCurrentRoundedTime(int $runEveryMin)
{
$time = new DateTime('now', $this->getCronTimeZone());
$time->modify('-' . $time->format('s') . ' sec');
$time->modify('-' . ($time->format('i') % $runEveryMin) . ' min');

return DateTimeImmutable::createFromMutable($time);
}

/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
Expand Down
67 changes: 67 additions & 0 deletions packages/framework/src/Command/ScheduleFeedsCommand.php
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Shopsys\FrameworkBundle\Command;

use Shopsys\FrameworkBundle\Model\Feed\FeedFacade;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'shopsys:feed-schedule')]
class ScheduleFeedsCommand extends Command
{
private const OPTION_FEED_NAME = 'feed-name';
private const OPTION_ALL = 'all';

/**
* @param \Shopsys\FrameworkBundle\Model\Feed\FeedFacade $feedFacade
*/
public function __construct(
protected readonly FeedFacade $feedFacade,
) {
parent::__construct();
}

/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDescription('Schedule feeds to be generated in the next cron run.')
->addOption(self::OPTION_FEED_NAME, null, InputOption::VALUE_OPTIONAL, 'name of feed to be scheduled')
->addOption(self::OPTION_ALL, null, InputOption::VALUE_NONE, 'schedule all feeds');
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$optionAll = $input->getOption(self::OPTION_ALL);
$optionFeedName = $input->getOption(self::OPTION_FEED_NAME);

$symfonyStyle = new SymfonyStyle($input, $output);

if ($optionAll === true) {
$symfonyStyle->info('Scheduling all feeds...');
$this->feedFacade->scheduleAllFeeds();
} elseif ($optionFeedName !== null) {
$symfonyStyle->info('Scheduling feed...');
$this->feedFacade->scheduleFeedByName($optionFeedName);
} else {
$symfonyStyle->error('You have to specify either --all or --feed-name option.');

return Command::FAILURE;
}

$symfonyStyle->success('Done!');

return Command::SUCCESS;
}
}
13 changes: 7 additions & 6 deletions packages/framework/src/Component/Cron/Config/CronConfig.php
Expand Up @@ -21,8 +21,9 @@ class CronConfig
/**
* @param \Shopsys\FrameworkBundle\Component\Cron\CronTimeResolver $cronTimeResolver
*/
public function __construct(protected readonly CronTimeResolver $cronTimeResolver)
{
public function __construct(
protected readonly CronTimeResolver $cronTimeResolver,
) {
$this->cronModuleConfigs = [];
}

Expand Down Expand Up @@ -52,7 +53,7 @@ public function registerCronModuleInstance(
throw new InvalidCronModuleException($serviceId);
}
$this->cronTimeResolver->validateTimeString($timeHours, 23, 1);
$this->cronTimeResolver->validateTimeString($timeMinutes, 55, 5);
$this->cronTimeResolver->validateTimeString($timeMinutes, 55, 1);

$cronModuleConfig = new CronModuleConfig($service, $serviceId, $timeHours, $timeMinutes, $readableName, $readableFrequency, $runEveryMin, $timeoutIteratedCronSec);
$cronModuleConfig->assignToInstance($instanceName);
Expand All @@ -63,7 +64,7 @@ public function registerCronModuleInstance(
/**
* @return \Shopsys\FrameworkBundle\Component\Cron\Config\CronModuleConfig[]
*/
public function getAllCronModuleConfigs()
public function getAllCronModuleConfigs(): array
{
return $this->cronModuleConfigs;
}
Expand All @@ -72,7 +73,7 @@ public function getAllCronModuleConfigs()
* @param \DateTimeInterface $roundedTime
* @return \Shopsys\FrameworkBundle\Component\Cron\Config\CronModuleConfig[]
*/
public function getCronModuleConfigsByTime(DateTimeInterface $roundedTime)
public function getCronModuleConfigsByTime(DateTimeInterface $roundedTime): array
{
$matchedCronConfigs = [];

Expand All @@ -89,7 +90,7 @@ public function getCronModuleConfigsByTime(DateTimeInterface $roundedTime)
* @param string $serviceId
* @return \Shopsys\FrameworkBundle\Component\Cron\Config\CronModuleConfig
*/
public function getCronModuleConfigByServiceId($serviceId)
public function getCronModuleConfigByServiceId(string $serviceId): CronModuleConfig
{
foreach ($this->cronModuleConfigs as $cronConfig) {
if ($cronConfig->getServiceId() === $serviceId) {
Expand Down
4 changes: 2 additions & 2 deletions packages/framework/src/Component/Cron/CronTimeInterface.php
Expand Up @@ -9,10 +9,10 @@ interface CronTimeInterface
/**
* @return string
*/
public function getTimeMinutes();
public function getTimeMinutes(): string;

/**
* @return string
*/
public function getTimeHours();
public function getTimeHours(): string;
}
6 changes: 3 additions & 3 deletions packages/framework/src/Component/Cron/CronTimeResolver.php
Expand Up @@ -14,7 +14,7 @@ class CronTimeResolver
* @param \DateTimeInterface $dateTime
* @return bool
*/
public function isValidAtTime(CronTimeInterface $cronTime, DateTimeInterface $dateTime)
public function isValidAtTime(CronTimeInterface $cronTime, DateTimeInterface $dateTime): bool
{
$hour = (int)$dateTime->format('G');
$minute = (int)$dateTime->format('i');
Expand All @@ -28,7 +28,7 @@ public function isValidAtTime(CronTimeInterface $cronTime, DateTimeInterface $da
* @param string $timeString
* @return bool
*/
protected function isMatchWithTimeString($value, $timeString)
protected function isMatchWithTimeString(int $value, string $timeString): bool
{
$timeValues = explode(',', $timeString);
$matches = null;
Expand All @@ -52,7 +52,7 @@ protected function isMatchWithTimeString($value, $timeString)
* @param int $maxValue
* @param int $divisibleBy
*/
public function validateTimeString($timeString, $maxValue, $divisibleBy)
public function validateTimeString(string $timeString, int $maxValue, int $divisibleBy): void
{
$timeValues = explode(',', $timeString);
$matches = null;
Expand Down

0 comments on commit df87fcd

Please sign in to comment.