Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\CommonPreConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\CountryConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\CurrencyConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\DateIntervalConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\DateTimeConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\EmailConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\FileConfigurator;
Expand Down Expand Up @@ -398,6 +399,8 @@

->set(CurrencyConfigurator::class)

->set(DateIntervalConfigurator::class)

->set(DateTimeConfigurator::class)
->arg(0, service(IntlFormatter::class))

Expand Down
3 changes: 2 additions & 1 deletion doc/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ These are all the built-in fields provided by EasyAdmin:
* :doc:`CountryField </fields/CountryField>`
* :doc:`CurrencyField </fields/CurrencyField>`
* :doc:`DateField </fields/DateField>`
* :doc:`DateIntervalField </fields/DateIntervalField>`
* :doc:`DateTimeField </fields/DateTimeField>`
* :doc:`EmailField </fields/EmailField>`
* :doc:`FileField </fields/FileField>`
Expand Down Expand Up @@ -743,7 +744,7 @@ Doctrine Type Recommended EasyAdmin Fields
``datetime`` ``DateTimeField``
``datetimetz_immutable`` ``DateTimeField``
``datetimetz`` ``DateTimeField``
``dateinterval`` ``TextField``
``dateinterval`` ``DateIntervalField``
``decimal`` ``NumberField``
``float`` ``NumberField``
``guid`` ``TextField``
Expand Down
47 changes: 47 additions & 0 deletions doc/fields/DateIntervalField.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
EasyAdmin Date Interval Field
=============================

This field is used to represent a value that stores a PHP ``DateInterval``
object (e.g. a duration mapped to Doctrine's ``dateinterval`` column type).

In :ref:`form pages (edit and new) <crud-pages>` it is rendered as a single
text input that expects an `ISO 8601 duration`_ pattern (for example ``P1Y2M3D``
for "1 year, 2 months and 3 days" or ``PT1H30M`` for "1 hour and 30 minutes").

In :ref:`read-only pages (index and detail) <crud-pages>` the value is rendered
as a localized, human-friendly string (for example ``2 years 4 days 6 hours
8 minutes``). Each part is translated and pluralized using the
``EasyAdminBundle`` translation domain.

Basic Information
-----------------

* **PHP Class**: ``EasyCorp\Bundle\EasyAdminBundle\Field\DateIntervalField``
* **Doctrine DBAL Type** used to store this value: ``dateinterval``
* **Symfony Form Type** used to render the field: `DateIntervalType`_
* **Rendered as**:

.. code-block:: html

<input type="text" placeholder="P1Y2M3DT4H5M6S">

Options
-------

setFormat
~~~~~~~~~

By default, in read-only pages (``index`` and ``detail``) date intervals are
displayed using a localized, pluralized representation built from the
``date_interval.*`` translation keys.

Use this option to override that default with a raw format string passed to
``DateInterval::format()``::

yield DateIntervalField::new('duration')->setFormat('%y years, %m months, %d days');

The same override is available globally on the CRUD configuration via
:ref:`Crud::setDateIntervalFormat() <crud-date-time-number-format-options>`.

.. _`ISO 8601 duration`: https://en.wikipedia.org/wiki/ISO_8601#Durations
.. _`DateIntervalType`: https://symfony.com/doc/current/reference/forms/types/dateinterval.html
6 changes: 3 additions & 3 deletions src/Dto/CrudDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ final class CrudDto
private ?string $timePattern = 'medium';
/** @var array{string, string} */
private array $dateTimePattern = ['medium', 'medium'];
private string $dateIntervalFormat = '%%y Year(s) %%m Month(s) %%d Day(s)';
private ?string $dateIntervalFormat = null;
private ?string $timezone = null;
private ?string $numberFormat = null;
private ?string $thousandsSeparator = null;
Expand Down Expand Up @@ -299,12 +299,12 @@ public function setDateTimePattern(string $dateFormatOrPattern, string $timeForm
$this->dateTimePattern = [$dateFormatOrPattern, $timeFormat];
}

public function getDateIntervalFormat(): string
public function getDateIntervalFormat(): ?string
{
return $this->dateIntervalFormat;
}

public function setDateIntervalFormat(string $format): void
public function setDateIntervalFormat(?string $format): void
{
$this->dateIntervalFormat = $format;
}
Expand Down
43 changes: 43 additions & 0 deletions src/Field/Configurator/DateIntervalConfigurator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;

use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateIntervalField;

/**
* @author Pascal CESCON <pascal.cescon@gmail.com>
*/
final class DateIntervalConfigurator implements FieldConfiguratorInterface
{
public function supports(FieldDto $field, EntityDto $entityDto): bool
{
return DateIntervalField::class === $field->getFieldFqcn();
}

public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
$value = $field->getValue();

if (!$value instanceof \DateInterval) {
return;
}

$format = $field->getCustomOption(DateIntervalField::OPTION_FORMAT)
?? $context->getCrud()?->getDateIntervalFormat();

if (null !== $format) {
$field->setFormattedValue($value->format($format));

return;
}

// sentinel: keep formattedValue non-null so the CommonPostConfigurator does not
// swap our template for label/null; the template builds the localized string
// from $field->getValue() (the original DateInterval).
$field->setFormattedValue('');
}
}
53 changes: 53 additions & 0 deletions src/Field/DateIntervalField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Field;

use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use Symfony\Component\Form\Extension\Core\Type\DateIntervalType;
use Symfony\Contracts\Translation\TranslatableInterface;

/**
* @author Pascal CESCON <pascal.cescon@gmail.com>
*/
final class DateIntervalField implements FieldInterface
{
use FieldTrait;

public const OPTION_FORMAT = 'format';

public static function new(string $propertyName, TranslatableInterface|string|bool|null $label = null): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setTemplateName('crud/field/date_interval')
->setFormType(DateIntervalType::class)
->setFormTypeOptions([
'widget' => 'single_text',
'input' => 'dateinterval',
'with_years' => true,
'with_months' => true,
'with_weeks' => false,
'with_days' => true,
'with_hours' => true,
'with_minutes' => true,
'with_seconds' => true,
])
->addCssClass('field-date-interval')
->setDefaultColumns('col-md-6 col-xxl-5')
->setCustomOption(self::OPTION_FORMAT, null);
}

/**
* Set a custom format string passed to DateInterval::format().
*
* When set, this overrides the localized rendering and the value is
* displayed using the raw DateInterval::format() output.
*/
public function setFormat(?string $format): self
{
$this->setCustomOption(self::OPTION_FORMAT, $format);

return $this;
}
}
16 changes: 16 additions & 0 deletions templates/crud/field/date_interval.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% if field.formattedValue is not empty %}
<time datetime="{{ field.value.format('P%yY%mM%dDT%hH%iM%sS') }}">{{ field.formattedValue }}</time>
{% else %}
{% set parts = [] %}
{% if field.value.y > 0 %}{% set parts = parts|merge([('date_interval.years'|trans({'%count%': field.value.y}, 'EasyAdminBundle'))]) %}{% endif %}
{% if field.value.m > 0 %}{% set parts = parts|merge([('date_interval.months'|trans({'%count%': field.value.m}, 'EasyAdminBundle'))]) %}{% endif %}
{% if field.value.d > 0 %}{% set parts = parts|merge([('date_interval.days'|trans({'%count%': field.value.d}, 'EasyAdminBundle'))]) %}{% endif %}
{% if field.value.h > 0 %}{% set parts = parts|merge([('date_interval.hours'|trans({'%count%': field.value.h}, 'EasyAdminBundle'))]) %}{% endif %}
{% if field.value.i > 0 %}{% set parts = parts|merge([('date_interval.minutes'|trans({'%count%': field.value.i}, 'EasyAdminBundle'))]) %}{% endif %}
{% if field.value.s > 0 %}{% set parts = parts|merge([('date_interval.seconds'|trans({'%count%': field.value.s}, 'EasyAdminBundle'))]) %}{% endif %}
{% if parts is empty %}{% set parts = [('date_interval.empty'|trans({}, 'EasyAdminBundle'))] %}{% endif %}
<time datetime="{{ field.value.format('P%yY%mM%dDT%hH%iM%sS') }}">{{ parts|join(' ') }}</time>
{% endif %}

Check failure on line 16 in templates/crud/field/date_interval.html.twig

View workflow job for this annotation

GitHub Actions / twig-linter

A file must end with 1 blank line; found 0
75 changes: 75 additions & 0 deletions tests/Unit/Field/DateIntervalFieldTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Unit\Field;

use EasyCorp\Bundle\EasyAdminBundle\Field\Configurator\DateIntervalConfigurator;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateIntervalField;

class DateIntervalFieldTest extends AbstractFieldTest
{
protected function setUp(): void
{
parent::setUp();

$this->configurator = new DateIntervalConfigurator();
}

public function testFieldWithoutValue(): void
{
$field = DateIntervalField::new('foo');
$field->setFieldFqcn(DateIntervalField::class);
$fieldDto = $this->configure($field);

$this->assertNull($fieldDto->getFormattedValue());
}

public function testFieldWithoutCustomFormatLeavesValueForTemplateRendering(): void
{
$field = DateIntervalField::new('foo')->setValue(new \DateInterval('P2Y4DT6H8M'));
$field->setFieldFqcn(DateIntervalField::class);
$fieldDto = $this->configure($field);

// sentinel empty string keeps CommonPostConfigurator from swapping the template
$this->assertSame('', $fieldDto->getFormattedValue());
$this->assertInstanceOf(\DateInterval::class, $fieldDto->getValue());
}

public function testFieldWithPerFieldFormat(): void
{
$field = DateIntervalField::new('foo')
->setValue(new \DateInterval('P1Y2M3D'))
->setFormat('%y years, %m months, %d days');
$field->setFieldFqcn(DateIntervalField::class);
$fieldDto = $this->configure($field);

$this->assertSame('1 years, 2 months, 3 days', $fieldDto->getFormattedValue());
}

public function testTemplateRendersLocalizedPluralizedString(): void
{
$field = DateIntervalField::new('foo')->setValue(new \DateInterval('P1Y2M3DT4H5M6S'));
$field->setFieldFqcn(DateIntervalField::class);
$fieldDto = $this->configure($field);

$html = $this->renderFieldTemplate($fieldDto, $this->entityDto, $this->adminContext);

$this->assertStringContainsString('1 year', $html);
$this->assertStringContainsString('2 months', $html);
$this->assertStringContainsString('3 days', $html);
$this->assertStringContainsString('4 hours', $html);
$this->assertStringContainsString('5 minutes', $html);
$this->assertStringContainsString('6 seconds', $html);
$this->assertStringContainsString('datetime="P1Y2M3DT4H5M6S"', $html);
}

public function testTemplateRendersZeroDurationAsEmptyLabel(): void
{
$field = DateIntervalField::new('foo')->setValue(new \DateInterval('PT0S'));
$field->setFieldFqcn(DateIntervalField::class);
$fieldDto = $this->configure($field);

$html = $this->renderFieldTemplate($fieldDto, $this->entityDto, $this->adminContext);

$this->assertStringContainsString('0 seconds', $html);
}
}
10 changes: 10 additions & 0 deletions translations/EasyAdminBundle.ar.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
'text_editor.view_content' => 'رؤية المحتوى',
],

'date_interval' => [
'years' => '{1} %count% year|]1,Inf] %count% years',
'months' => '{1} %count% month|]1,Inf] %count% months',
'days' => '{1} %count% day|]1,Inf] %count% days',
'hours' => '{1} %count% hour|]1,Inf] %count% hours',
'minutes' => '{1} %count% minute|]1,Inf] %count% minutes',
'seconds' => '{1} %count% second|]1,Inf] %count% seconds',
'empty' => '0 seconds',
],

'action' => [
'entity_actions' => 'إجراءات',
'new' => '%entity_label_singular% جديد',
Expand Down
10 changes: 10 additions & 0 deletions translations/EasyAdminBundle.bg.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
'text_editor.view_content' => 'Преглед на съдържание',
],

'date_interval' => [
'years' => '{1} %count% year|]1,Inf] %count% years',
'months' => '{1} %count% month|]1,Inf] %count% months',
'days' => '{1} %count% day|]1,Inf] %count% days',
'hours' => '{1} %count% hour|]1,Inf] %count% hours',
'minutes' => '{1} %count% minute|]1,Inf] %count% minutes',
'seconds' => '{1} %count% second|]1,Inf] %count% seconds',
'empty' => '0 seconds',
],

'action' => [
'entity_actions' => 'Действия',
'new' => 'Добавяне на %entity_label_singular%',
Expand Down
10 changes: 10 additions & 0 deletions translations/EasyAdminBundle.ca.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
'text_editor.view_content' => 'Veure contingut',
],

'date_interval' => [
'years' => '{1} %count% year|]1,Inf] %count% years',
'months' => '{1} %count% month|]1,Inf] %count% months',
'days' => '{1} %count% day|]1,Inf] %count% days',
'hours' => '{1} %count% hour|]1,Inf] %count% hours',
'minutes' => '{1} %count% minute|]1,Inf] %count% minutes',
'seconds' => '{1} %count% second|]1,Inf] %count% seconds',
'empty' => '0 seconds',
],

'action' => [
'entity_actions' => 'Accions',
'new' => 'Crear %entity_label_singular%',
Expand Down
10 changes: 10 additions & 0 deletions translations/EasyAdminBundle.cs.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
'text_editor.view_content' => 'Zobrazit obsah',
],

'date_interval' => [
'years' => '{1} %count% year|]1,Inf] %count% years',
'months' => '{1} %count% month|]1,Inf] %count% months',
'days' => '{1} %count% day|]1,Inf] %count% days',
'hours' => '{1} %count% hour|]1,Inf] %count% hours',
'minutes' => '{1} %count% minute|]1,Inf] %count% minutes',
'seconds' => '{1} %count% second|]1,Inf] %count% seconds',
'empty' => '0 seconds',
],

'action' => [
'entity_actions' => 'Akce',
'new' => 'Vytvořit %entity_label_singular%',
Expand Down
10 changes: 10 additions & 0 deletions translations/EasyAdminBundle.da.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
'text_editor.view_content' => 'Vis indhold',
],

'date_interval' => [
'years' => '{1} %count% year|]1,Inf] %count% years',
'months' => '{1} %count% month|]1,Inf] %count% months',
'days' => '{1} %count% day|]1,Inf] %count% days',
'hours' => '{1} %count% hour|]1,Inf] %count% hours',
'minutes' => '{1} %count% minute|]1,Inf] %count% minutes',
'seconds' => '{1} %count% second|]1,Inf] %count% seconds',
'empty' => '0 seconds',
],

'action' => [
'entity_actions' => 'Handlinger',
'new' => 'Tilføj %entity_label_singular%',
Expand Down
Loading
Loading