Skip to content

Commit 5058fa3

Browse files
committed
Add initial set of files
0 parents  commit 5058fa3

13 files changed

Lines changed: 733 additions & 0 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
composer.lock
3+
.phpunit.result.cache
4+
var/

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
# sourecode/timezone-bundle
3+
4+
This bundle provides a simple way to manage timezones in Symfony applications.
5+
Unfortunately, there is no way to set the timezone in the request object, so you have to get it from the `TimezoneManager`.
6+
Except from that, the usage is pretty simple and the same as for the locale.
7+
8+
## Installation
9+
10+
Make sure Composer is installed globally, as explained in the
11+
[installation chapter](https://getcomposer.org/doc/00-intro.md)
12+
of the Composer documentation.
13+
14+
### Applications that use Symfony Flex
15+
16+
Open a command console, enter your project directory and execute:
17+
18+
```console
19+
composer require sourecode/timezone-bundle
20+
```
21+
22+
### Applications that don't use Symfony Flex
23+
24+
#### Step 1: Download the Bundle
25+
26+
Open a command console, enter your project directory and execute the
27+
following command to download the latest stable version of this bundle:
28+
29+
```console
30+
composer require sourecode/timezone-bundle
31+
```
32+
33+
#### Step 2: Enable the Bundle
34+
35+
Then, enable the bundle by adding it to the list of registered bundles
36+
in the `config/bundles.php` file of your project:
37+
38+
```php
39+
// config/bundles.php
40+
41+
return [
42+
// ...
43+
\SoureCode\Bundle\Timezone\SoureCodeTimezoneBundle::class => ['all' => true],
44+
];
45+
```
46+
47+
## Config
48+
49+
```yaml
50+
# config/packages/soure_code_timezone.yaml
51+
soure_code_timezone:
52+
# if default_timezone is empty, the default timezone is 'UTC'
53+
default_timezone: 'Asia/Tokyo'
54+
# if enabled_timezones is empty, all timezones are enabled
55+
enabled_timezones: ['UTC', 'Europe/Berlin', 'Asia/Tokyo', 'Australia/Sydney']
56+
```

composer.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "sourecode/timezone-bundle",
3+
"type": "symfony-bundle",
4+
"license": "MIT",
5+
"autoload": {
6+
"psr-4": {
7+
"SoureCode\\Bundle\\Timezone\\": "src/"
8+
}
9+
},
10+
"autoload-dev": {
11+
"psr-4": {
12+
"SoureCode\\Bundle\\Timezone\\Tests\\": "tests/",
13+
"App\\": "tests/app/src/"
14+
}
15+
},
16+
"authors": [
17+
{
18+
"name": "chapterjason",
19+
"email": "jason@sourecode.dev"
20+
}
21+
],
22+
"require": {
23+
"ext-pcntl": "*",
24+
"symfony/http-kernel": "^7.1",
25+
"symfony/config": "^7.1",
26+
"symfony/dependency-injection": "^7.1",
27+
"symfony/intl": "^7.1"
28+
},
29+
"require-dev": {
30+
"symfony/clock": "^7.1",
31+
"twig/twig": "^v3.2",
32+
"symfony/browser-kit": "^7.1",
33+
"nyholm/symfony-bundle-test": "^3.0",
34+
"phpunit/phpunit": "^9.5",
35+
"symfony/phpunit-bridge": "^7.1",
36+
"symfony/security-bundle": "^7.1",
37+
"symfony/runtime": "^7.1"
38+
},
39+
"config": {
40+
"allow-plugins": {
41+
"symfony/runtime": true
42+
}
43+
}
44+
}

phpunit.xml.dist

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
4+
backupGlobals="false"
5+
backupStaticAttributes="false"
6+
colors="false"
7+
convertErrorsToExceptions="true"
8+
convertNoticesToExceptions="true"
9+
convertWarningsToExceptions="true"
10+
processIsolation="false"
11+
stopOnFailure="false"
12+
bootstrap="./vendor/autoload.php"
13+
>
14+
<coverage>
15+
<include>
16+
<directory suffix=".php">./</directory>
17+
</include>
18+
<exclude>
19+
<directory>vendor</directory>
20+
<directory>tests</directory>
21+
</exclude>
22+
</coverage>
23+
<testsuites>
24+
<testsuite name="Test Suite">
25+
<directory>./tests</directory>
26+
</testsuite>
27+
</testsuites>
28+
<php>
29+
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
30+
</php>
31+
</phpunit>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace SoureCode\Bundle\Timezone\EventListener;
4+
5+
use SoureCode\Bundle\Timezone\Manager\TimezoneManager;
6+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpFoundation\RequestStack;
9+
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
10+
use Symfony\Component\HttpKernel\Event\KernelEvent;
11+
use Symfony\Component\HttpKernel\Event\RequestEvent;
12+
use Symfony\Component\HttpKernel\KernelEvents;
13+
use Symfony\Component\Routing\RequestContextAwareInterface;
14+
15+
final readonly class TimezoneListener implements EventSubscriberInterface
16+
{
17+
public function __construct(
18+
private TimezoneManager $timezoneManager,
19+
private RequestStack $requestStack,
20+
private ?RequestContextAwareInterface $router = null,
21+
private string $defaultTimezone = 'UTC',
22+
)
23+
{
24+
}
25+
26+
public function setDefaultTimezone(KernelEvent $event): void
27+
{
28+
$this->timezoneManager->setTimezone($this->defaultTimezone);
29+
}
30+
31+
public function onKernelFinishRequest(FinishRequestEvent $event): void
32+
{
33+
if (null !== $parentRequest = $this->requestStack->getParentRequest()) {
34+
$this->setRouterContext($parentRequest);
35+
}
36+
}
37+
38+
public function onKernelRequest(RequestEvent $event): void
39+
{
40+
$request = $event->getRequest();
41+
42+
$this->setTimezone($request);
43+
$this->setRouterContext($request);
44+
}
45+
46+
private function setTimezone(Request $request): void
47+
{
48+
if ($timezone = $request->attributes->get('_timezone')) {
49+
$this->timezoneManager->setTimezone($timezone);
50+
}
51+
52+
if (!$request->hasPreviousSession()) {
53+
return;
54+
}
55+
56+
if ($timezone = $request->getSession()->get('_timezone')) {
57+
$this->timezoneManager->setTimezone($timezone);
58+
}
59+
}
60+
61+
private function setRouterContext(Request $request): void
62+
{
63+
$this->router?->getContext()->setParameter('_timezone', $request->headers->get('X-Timezone') ?? $this->defaultTimezone);
64+
}
65+
66+
public static function getSubscribedEvents(): array
67+
{
68+
return [
69+
KernelEvents::REQUEST => [
70+
['setDefaultTimezone', 100],
71+
// must be registered after the Router to have access to the _timezone
72+
['onKernelRequest', 16],
73+
],
74+
KernelEvents::FINISH_REQUEST => [['onKernelFinishRequest', 0]],
75+
];
76+
}
77+
}

src/Manager/TimezoneManager.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace SoureCode\Bundle\Timezone\Manager;
4+
5+
use DateTimeZone;
6+
use Symfony\Component\Clock\ClockInterface;
7+
use Symfony\Component\Intl\Timezones;
8+
use Twig\Environment;
9+
use Twig\Extension\CoreExtension;
10+
11+
class TimezoneManager
12+
{
13+
private static TimezoneManager $instance;
14+
private DateTimeZone $timezone;
15+
16+
public function __construct(
17+
private array $enabledTimezoneNames = [],
18+
private readonly ?ClockInterface $clock = null,
19+
private readonly ?Environment $twig = null,
20+
)
21+
{
22+
if (empty($this->enabledTimezoneNames)) {
23+
$this->enabledTimezoneNames = ['UTC', ...Timezones::getIds()];
24+
}
25+
26+
$this->timezone = new DateTimeZone('UTC');
27+
28+
self::setInstance($this);
29+
}
30+
31+
private static function setInstance(self $timezone): void
32+
{
33+
self::$instance = $timezone;
34+
}
35+
36+
public static function getInstance(): TimezoneManager
37+
{
38+
// in case of getting called from a command or messenger. (console)
39+
if (!isset(self::$instance)) {
40+
new self();
41+
}
42+
43+
return self::$instance;
44+
}
45+
46+
public function setTimezone(DateTimeZone|string $value): void
47+
{
48+
if (is_string($value)) {
49+
$value = new DateTimeZone($value);
50+
}
51+
52+
if (!in_array($value->getName(), $this->enabledTimezoneNames, true)) {
53+
throw new \InvalidArgumentException(sprintf('The timezone "%s" is not enabled.', $value->getName()));
54+
}
55+
56+
$this->timezone = $value;
57+
58+
$this->clock?->withTimeZone($this->timezone);
59+
$this->twig?->getExtension(CoreExtension::class)->setTimezone($this->timezone);
60+
61+
date_default_timezone_set($this->timezone->getName());
62+
}
63+
64+
public function getTimezone(): DateTimeZone
65+
{
66+
return $this->timezone;
67+
}
68+
}

src/SoureCodeTimezoneBundle.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace SoureCode\Bundle\Timezone;
4+
5+
use SoureCode\Bundle\Timezone\EventListener\TimezoneListener;
6+
use SoureCode\Bundle\Timezone\Manager\TimezoneManager;
7+
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
8+
use Symfony\Component\DependencyInjection\ContainerBuilder;
9+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
10+
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
11+
use Symfony\Component\Intl\Timezones;
12+
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
13+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
14+
15+
class SoureCodeTimezoneBundle extends AbstractBundle
16+
{
17+
private static string $PREFIX = 'soure_code.timezone.';
18+
19+
public function configure(DefinitionConfigurator $definition): void
20+
{
21+
$timezones = ['UTC', ...Timezones::getIds()];
22+
23+
// @formatter:off
24+
$definition->rootNode()
25+
->fixXmlConfig('timezone')
26+
->children()
27+
->scalarNode('default_timezone')
28+
->defaultValue('UTC')
29+
->info('The default timezone.')
30+
->validate()
31+
->ifTrue(fn ($v) => !in_array($v, $timezones, true))
32+
->thenInvalid('The timezone "%s" is not valid.')
33+
->end()
34+
->end()
35+
->arrayNode('enabled_timezones')
36+
->scalarPrototype()->end()
37+
->info('List of enabled timezones. If empty, all timezones are enabled.')
38+
->validate()
39+
->ifTrue(fn (array $values) => count(array_diff($values, $timezones)) > 0)
40+
->then(fn (array $values) => throw new \InvalidArgumentException(sprintf('The timezones "%s" are not valid.', implode('", "', array_diff($values, $timezones)))))
41+
->end()
42+
->end()
43+
->end()
44+
;
45+
// @formatter:on
46+
}
47+
48+
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
49+
{
50+
$parameters = $container->parameters();
51+
52+
$parameters->set(self::$PREFIX . 'default_timezone', $config['default_timezone']);
53+
$parameters->set(self::$PREFIX . 'enabled_timezones', $config['enabled_timezones']);
54+
55+
$services = $container->services();
56+
57+
$services->set(self::$PREFIX . 'manager', TimezoneManager::class)
58+
->args([
59+
param(self::$PREFIX . 'enabled_timezones'),
60+
service('clock')->ignoreOnInvalid(),
61+
service('twig')->ignoreOnInvalid(),
62+
]);
63+
64+
$services->alias(TimezoneManager::class, self::$PREFIX . 'manager')
65+
->public();
66+
67+
$services
68+
->set(self::$PREFIX.'listener', TimezoneListener::class)
69+
->args([
70+
service(self::$PREFIX.'manager'),
71+
service('request_stack'),
72+
service('router')->ignoreOnInvalid(),
73+
param(self::$PREFIX.'default_timezone'),
74+
])
75+
->tag('kernel.event_subscriber');
76+
}
77+
}
78+

0 commit comments

Comments
 (0)