PHP: generator fun with sun instants

I was reading the Occurfy post and I saw code like ITimeline sunsets = AstroInstants.LocalSunset; ITimeline twentyMinAfterSunset = sunsets + TimeSpan.FromMinutes(20); And I wanted to mimic this in PHP. Building the class I'm using PHP 8.4 features, so that is the minimum version. I'm going to use constructor or fluent API methods to set the location and the possibility to offset the instants. public function __construct( public private(set) float $latitude = 0.0, public private(set) float $longitude = 0.0, public private(set) string $location = 'UTC', public private(set) string $sooner = '', public private(set) string $later = '', ) { if ($location != 'UTC') { try { $location = new DateTimeZone($location)->getLocation(); $this->latitude = $location['latitude']; $this->longitude = $location['longitude']; } catch (DateInvalidTimeZoneException $exception) { throw new SunInstantsNoLocation('Invalid dateTimeZone value added.'); } } // These checks return a DateMalformedIntervalStringException when the string is invalid if ($sooner != '') { DateInterval::createFromDateString($sooner); } if ($later != '') { DateInterval::createFromDateString($later); } } public function addLocation(string $dateTimeZone) { try { $location = new DateTimeZone($dateTimeZone)->getLocation(); $this->latitude = $location['latitude']; $this->longitude = $location['longitude']; return $this; } catch (DateInvalidTimeZoneException $exception) { throw new SunInstantsNoLocation('Invalid dateTimeZone value added.'); } } public function addSooner(string $relativeDate) { $this->sooner = DateInterval::createFromDateString($relativeDate); return $this; } public function addLater(string $relativeDate) { $this->later = DateInterval::createFromDateString($relativeDate); return $this; } I'm using asymmetric visibility for the constructor arguments because I want the properties only to be changed by the fluent API methods. This allows you to do: new SunInstants(location: 'Europe/Brussels') or new SunInstants()->addLocation('Europe/Brussels') new SunInstants(sooner: '30 minutes') or new SunInstants()->addSooner('30 minutes') new SunInstants(later: '30 minutes') or new SunInstants()->addLater('30 minutes') The sun instants come from the date_sun_info function. So all I had to do is create public functions for the different instants, sunset/sunrise/zenith, to generate them. public function getSunsets(string $start = 'now', string $end = '') : Generator { return $this->get('sunset', $start, $end); } public function getSunrises(string $start = 'now', string $end = '') : Generator { return $this->get('sunrise', $start, $end); } public function getZeniths(string $start = 'now', string $end = '') : Generator { return $this->get('transit', $start, $end); } private function get(string $key, string $start = 'now', string $end = '') : Generator { if ($this->latitude == 0.0) { throw new SunInstantsNoLocation('Add the latitude and longitude using the constructor or the fluent methods.'); } // $start and $end will produce a DateMalformedStringException when the string is invalid. $start = new DateTimeImmutable($start); // prevent an endless loop if($end == '') { $end = '+1 year'; } $end = new DateTimeImmutable($end); do { $sunInfo = date_sun_info($start->getTimestamp(), $this->latitude, $this->longitude); yield $this->sunInfoToDateTime($sunInfo[$key]); $start = $start->add(new DateInterval('P1D')); } while($start->format('YMD') != $end->format('YMD')); $sunInfo = date_sun_info($end->getTimestamp(), $this->latitude, $this->longitude); yield $this->sunInfoToDateTime($sunInfo[$key]); } private function sunInfoToDateTime(int $sunInfo) { $date = new DateTimeImmutable()->setTimestamp($sunInfo); if($this->later != '') { $date = $date->add(DateInterval::createFromDateString($this->later)); } if($this->sooner != '') { $date = $date->sub(DateInterval::createFromDateString($this->sooner)); } return $date; } As you can see the get method is the core of the class. For the people that are not familiar with generators. A generator is a special implementation of the Iterator interface. If you want an iterator class you need to implement multiple methods. A Generator allows you, by using the yield keyword, to create a forward-only iterator

Apr 6, 2025 - 10:51
 0
PHP: generator fun with sun instants

I was reading the Occurfy post and I saw code like

ITimeline sunsets = AstroInstants.LocalSunset;
ITimeline twentyMinAfterSunset = sunsets + TimeSpan.FromMinutes(20);

And I wanted to mimic this in PHP.

Building the class

I'm using PHP 8.4 features, so that is the minimum version.

I'm going to use constructor or fluent API methods to set the location and the possibility to offset the instants.

public function __construct(
        public private(set) float $latitude = 0.0,
        public private(set) float $longitude = 0.0,
        public private(set) string $location = 'UTC',
        public private(set) string $sooner = '',
        public private(set) string $later = '',
)
    {
        if ($location != 'UTC') {
            try {
                $location = new DateTimeZone($location)->getLocation();

                $this->latitude = $location['latitude'];
                $this->longitude = $location['longitude'];
            } catch (DateInvalidTimeZoneException $exception) {
                throw new SunInstantsNoLocation('Invalid dateTimeZone value added.');
            }
        }
        // These checks return a DateMalformedIntervalStringException when the string is invalid
        if ($sooner != '') {
            DateInterval::createFromDateString($sooner);
        }

        if ($later != '') {
            DateInterval::createFromDateString($later);
        }
    }

    public function addLocation(string $dateTimeZone)
    {
        try {
            $location = new DateTimeZone($dateTimeZone)->getLocation();

            $this->latitude = $location['latitude'];
            $this->longitude = $location['longitude'];

            return $this;
        } catch (DateInvalidTimeZoneException $exception) {
            throw new SunInstantsNoLocation('Invalid dateTimeZone value added.');
        }
    }

    public function addSooner(string $relativeDate)
    {
        $this->sooner = DateInterval::createFromDateString($relativeDate);

        return $this;
    }

    public function addLater(string $relativeDate)
    {
        $this->later = DateInterval::createFromDateString($relativeDate);

        return $this;
    }

I'm using asymmetric visibility for the constructor arguments because I want the properties only to be changed by the fluent API methods.

This allows you to do:

  • new SunInstants(location: 'Europe/Brussels') or new SunInstants()->addLocation('Europe/Brussels')

  • new SunInstants(sooner: '30 minutes') or new SunInstants()->addSooner('30 minutes')

  • new SunInstants(later: '30 minutes') or new SunInstants()->addLater('30 minutes')

The sun instants come from the date_sun_info function. So all I had to do is create public functions for the different instants, sunset/sunrise/zenith, to generate them.

public function getSunsets(string $start = 'now', string $end = '') : Generator
     {
        return $this->get('sunset', $start, $end);
     }

    public function getSunrises(string $start = 'now', string $end = '') : Generator
    {
        return $this->get('sunrise', $start, $end);
    }

    public function getZeniths(string $start = 'now', string $end = '') : Generator
    {
        return $this->get('transit', $start, $end);
    }

    private function get(string $key, string $start = 'now', string $end = '') : Generator
    {
        if ($this->latitude == 0.0) {
            throw new SunInstantsNoLocation('Add the latitude and longitude using the constructor or the fluent methods.');
        }

        // $start and $end will produce a DateMalformedStringException when the string is invalid.
        $start = new DateTimeImmutable($start);
        // prevent an endless loop
        if($end == '') {
            $end = '+1 year';
        }

        $end = new DateTimeImmutable($end);

        do {
            $sunInfo = date_sun_info($start->getTimestamp(), $this->latitude, $this->longitude);

            yield $this->sunInfoToDateTime($sunInfo[$key]);

            $start = $start->add(new DateInterval('P1D'));
        } while($start->format('YMD') != $end->format('YMD'));

        $sunInfo = date_sun_info($end->getTimestamp(), $this->latitude, $this->longitude);

        yield $this->sunInfoToDateTime($sunInfo[$key]);
    }

    private function sunInfoToDateTime(int $sunInfo)
    {
        $date = new DateTimeImmutable()->setTimestamp($sunInfo);

        if($this->later != '') {
            $date = $date->add(DateInterval::createFromDateString($this->later));
        }

        if($this->sooner != '') {
            $date = $date->sub(DateInterval::createFromDateString($this->sooner));
        }

        return $date;
    }

As you can see the get method is the core of the class.

For the people that are not familiar with generators. A generator is a special implementation of the Iterator interface.
If you want an iterator class you need to implement multiple methods.
A Generator allows you, by using the yield keyword, to create a forward-only iterator. So you don't need to implement the required methods.

The biggest benefit of using using a iterator in PHP is memory efficiency. The thing, something like a file or an array, that needs iterating is not loaded ahead of calling the iterator.

I placed the actual generation of the sun instant in the sunInfoToDateTime method.

This allows you to do

$thisMonthsSunsets = new SunInstants(location: 'Europe/Brussels')
       ->getSunsets('first day of this month', 'last day of this month');

foreach($thisMonthsSunsets as $sunset) {
   echo $sunset->format('d-m-Y H:i:s') . PHP_EOL;
}

Conclusion

When I started I didn't know the date_sun_info function existed.
The date_sunrise and date_sunset also exist, but they are deprecated from PHP 8.1 and will be removed in PHP 9.0.

While I'm not a fan of strings in code, in this case it makes the code more readable.

It was a fun little exercise.