Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Datetime Parts Laravel Package

digital-craftsman/datetime-parts

Value objects for precise date/time parts in PHP: Moment (UTC-based) plus Time, Date, Month, Year, Day/Weekday and collections. Avoid misleading DateTime comparisons, handle timezone-safe modifications across DST, with Symfony normalizers and Doctrine types.

View on GitHub
Deep Wiki
Context7
## Getting Started

### Minimal Setup
1. **Installation**:
   ```bash
   composer require digital-craftsman/date-time-precision:0.14.*

Pin to minor version to avoid breaking changes.

  1. Basic Usage: Replace \DateTime with Moment for precise time handling:

    use DigitalCraftsman\DateTimePrecision\Moment;
    use DigitalCraftsman\DateTimePrecision\Time;
    
    $now = Moment::now(); // UTC by default
    $openingTime = Time::fromString('09:00', 'Europe/Berlin');
    
  2. First Use Case: Validate business hours in a specific timezone:

    if ($now->isBeforeInTimeZone($openingTime, new \DateTimeZone('Europe/Berlin'))) {
        throw new FacilityClosedException();
    }
    

Key Entry Points

  • Moment: For exact timestamps (replaces \DateTime).
  • Time/Date/Month/Year: For timezone-agnostic components.
  • Doctrine Types: Store these objects directly in databases (auto-registered).

Implementation Patterns

Core Workflows

  1. Timezone-Agnostic Comparisons:

    // Compare dates without timezone pollution
    $bookingDate = Date::fromString('2024-01-15');
    if ($now->isDateBeforeInTimeZone($bookingDate, $userTimezone)) {
        // Handle future booking
    }
    
  2. Immutable Modifications:

    // Modify a moment in a specific timezone
    $nextWeek = $now->modifyInTimeZone('+7 days', $facilityTimezone);
    
  3. Database Integration:

    #[ORM\Column(type: Moment::class)]
    private Moment $createdAt;
    

Integration Tips

  • Clock Abstraction: Use Clock interface for testing (automatically injects FrozenClock in tests):

    public function __construct(private Clock $clock) {}
    
  • Normalization: Works seamlessly with Symfony serializers (e.g., API Platform):

    # config/packages/api_platform.yaml
    api_platform:
        formats:
            jsonld: ['application/ld+json']
            json: ['application/json']
    
  • Collections: Use Days or Weekdays for bulk operations:

    $weekdays = Weekdays::fromString('Mon,Wed,Fri');
    

Common Patterns

  1. Validation:

    $eventDate->mustBeAfterOrEqualTo($now, new InvalidDateException());
    
  2. Period Calculations:

    $holidays = Date::fromString('2024-12-25')->datesUntil(
        Date::fromString('2025-01-01'),
        PeriodLimit::EXCLUDING_END
    );
    
  3. Timezone-Safe Formatting:

    $formatted = $moment->formatInTimeZone('Y-m-d H:i', $userTimezone);
    

Gotchas and Tips

Pitfalls

  1. UTC Default: All Moment objects are always stored in UTC internally. Modifications in other timezones are converted back to UTC.

    • Fix: Use modifyInTimeZone() explicitly for timezone-aware operations.
  2. Breaking Changes:

    • Minor versions may rename methods (e.g., isDateBeforeisDateBeforeInTimeZone).
    • Tip: Pin to 0.14.* and check UPGRADE.md.
  3. Doctrine Quirks:

    • Ensure your database supports DATETIME(6) for millisecond precision.
    • Tip: Use requiresSQLCommentHint() for custom SQL types.
  4. Immutability: All modifications return new instances. Avoid chaining without assignment:

    // ❌ Loses reference
    $now->modify('+1 day')->modify('+1 hour');
    
    // ✅ Correct
    $tomorrow = $now->modify('+1 day');
    $tomorrowAtNoon = $tomorrow->modify('+12 hours');
    

Debugging Tips

  1. Timezone Confusion: Use formatInTimeZone() to inspect values:

    $moment->formatInTimeZone('Y-m-d H:i P', $timezone);
    
  2. Comparison Issues: Prefer isBeforeInTimeZone() over diff() for clarity:

    // ❌ Ambiguous
    $now->diff($eventDate)->days > 0;
    
    // ✅ Explicit
    $now->isBeforeInTimeZone($eventDate, $timezone);
    
  3. Testing:

    • Use FrozenClock to mock time:
      $clock = new FrozenClock(Moment::fromString('2024-01-01'));
      $service = new MyService($clock);
      
    • Tip: Extend FrozenClock for custom time travel:
      class TestClock extends FrozenClock {
          public function advance(string $interval) {
              $this->setTime($this->getTime()->modify($interval));
          }
      }
      

Extension Points

  1. Custom Value Objects: Extend AbstractPrecisionDateTime for domain-specific types:

    class BusinessDay extends AbstractPrecisionDateTime {
        public static function fromDate(Date $date): self {
            return new self($date->toDateTimeInTimeZone(new \DateTimeZone('UTC')));
        }
    }
    
  2. Doctrine Types: Register custom types in src/Doctrine/DBAL/Types/:

    use DigitalCraftsman\DateTimePrecision\Doctrine\Types\MomentType;
    
    class CustomMomentType extends MomentType {
        public function convertToDatabaseValue($value, AbstractPlatform $platform) {
            // Custom logic
        }
    }
    
  3. Normalizers: Extend SelfAwareNormalizer for custom serialization:

    use DigitalCraftsman\SelfAwareNormalizers\Normalizer\SelfAwareNormalizer;
    
    class MomentNormalizer extends SelfAwareNormalizer {
        public function normalize($object, $format = null, array $context = []) {
            return ['@id' => $object->getIso(), 'timezone' => $object->getTimezone()->getName()];
        }
    }
    

Performance Notes

  • Avoid toDateTime() in Loops: The internal DateTime conversion is lightweight, but cache results if reused:

    private $cachedDateTime;
    
    public function getDateTimeInTimeZone(\DateTimeZone $timezone): \DateTime {
        return $this->cachedDateTime ??= $this->toDateTimeInTimeZone($timezone);
    }
    
  • Batch Operations: Use Days::fromRange() or Month::yearsUntil() for bulk operations instead of loops.


```markdown
### Configuration Quirks
1. **Symfony Auto-Wiring**:
   Ensure `Clock` is tagged as a service if not using autowiring:
   ```yaml
   services:
       DigitalCraftsman\DateTimePrecision\Clock\SystemClock:
           tags: ['clock']
  1. Doctrine Auto-Registration: If types aren’t auto-registered, manually add to config/packages/doctrine.yaml:

    doctrine:
        dbal:
            types:
                moment: DigitalCraftsman\DateTimePrecision\Doctrine\Types\MomentType
                date: DigitalCraftsman\DateTimePrecision\Doctrine\Types\DateType
    
  2. Timezone Handling:

    • The package ignores PHP’s default timezone. Always pass \DateTimeZone explicitly.
    • Tip: Store timezones as strings in your DB (e.g., VARCHAR(50)) and hydrate with new \DateTimeZone().

Testing Gotchas

  1. FrozenClock Scope: FrozenClock is environment-specific (test vs. prod). Verify it’s active:

    $this->assertInstanceOf(FrozenClock::class, $this->clock);
    
  2. Timezone Mismatches: Test comparisons across timezones:

    $utcNow = Moment::fromString('2024-01-01T00:00:00Z');
    $berlinNow = $utcNow->modifyInTimeZone('+1 hour', new \DateTimeZone('Europe/Berlin'));
    $this->assertTrue($utcNow->isBeforeInTimeZone($berlinNow, new \DateTimeZone('Europe/Berlin')));
    
  3. Edge Cases: Test DST transitions:

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor