Calendarful
Calendarful is a simple and easily extendable PHP solution that allows the generation of occurrences of recurrent events, thus eliminating the need to store hundreds or maybe thousands of occurrences in a database or other methods of storage.
This package ships with default implementations of interfaces for use out of the box although it is very simple to provide your own implementations if needs be.
It is compliant with PSR-2.
Installation
This package can be installed via Composer:
$ composer require plummer/calendarful
It requires PHP >= 5.3.0.
Required Set Up
Before using this package, a few steps need to be taken.
Event Models
This package requires that you have Event
and RecurrentEvent
models.For the models to be compatible with this package they must implement the relevant package interfaces.
The Event
model represents standard non-recurrent events and follows the EventInterface
:
<?php
use Plummer\Calendarful\Event\EventInterface;
class Event implements EventInterface
{
}
The RecurrentEvent
model represents recurrent events and follows the RecurrentEventInterface
:
<?php
use Plummer\Calendarful\Event\RecurrentEventInterface;
class RecurrentEvent implements RecurrentEventInterface
{
}
The RecurrentEventInterface
actually extends the EventInterface
in order for its implementations to provide a little more functionality that is specific to recurrent events.
Ideally, there should be properties on the models that are relevant to each getter and setter method
of the interface they implement, in order for this package to function most effectively.
For example, there should be a 'start date' property that getStartDate()
and setStartDate()
apply to.
The MockEvent
and MockRecurrentEvent
classes under the tests
directory provide an example implementation of the methods and relevant properties to be used.
Documentation of each method inside the EventInterface
and RecurrentEventInterface
files also provide brief explanations of the purpose of each of the properties.
If you don't like the idea of having two Event
models, you could have just one that implements the RecurrentEventInterface
. This would mean it would contain all of the functionality of standard non-recurrent events as well as recurrent ones, although it is not best practice as recurrent event related methods would be redundant for non-recurrent events. If you decide to take this path (not recommended), your EventRegistry
coming up can deal with the same model in both methods but return only applicable events.
Event Registry
Once you have your models fully set up, you need to create an EventRegistry
class which should implement the EventRegistryInterface
.
This should consist of two methods; one to retrieve events that recur (that follow the RecurrentEventInterface
) and the other to retrieve standard non-recurrent events (that follow the EventInterface
).
Calendarful performs different operations on the two different types.
This is my example Event Registry using Laravel's Eloquent ORM:
<?php
use \Plummer\Calendarful\Event\EventRegistryInterface;
class EventRegistry implements EventRegistryInterface
{
public function getEvents(array $filters = array())
{
$events = [];
$results = \TestEvent::where('startDate', '<=', $filters['toDate']->format('Y-m-d'))
->where('endDate', '>=', $filters['fromDate']->format('Y-m-d'))
->whereNull('type')->get();
foreach($results as $event) {
$events[$event->getId()] = $event;
}
return $events;
}
public function getRecurrentEvents(array $filters = array())
{
$recurrentEvents = [];
$results = \TestRecurrentEvent::whereNotNull('type')
->where(
function ($query) use ($filters) {
$query->whereNull('until')
->where('until', '>=', $filters['fromDate']->format('Y-m-d'), 'or');
})->get();
foreach($results as $event) {
$recurrentEvents[$event->getId()] = $event;
}
return $recurrentEvents;
}
}
When you populate the default Calendar
class with events, the parameters you pass will be passed to the
EventRegistry
as the $filters
you can see being used above. These passed $filters
allow the EventRegistry
to do a lot of the work in returning only the relevant events.
The EventRegistry
above uses the date filters to determine which events fall into the date range given.
If no filters are provided and all events are returned, the Calendar
class will determine which of those events are relevant.
The sole reason for filters being passed to the Event Registry is to optimize performance by using relevant events earlier in the process.
Usage
With the models and registry set up, you just need to instantiate the EventRegistry
and Calendar
and populate the Calendar
.
The populate
method takes in the EventRegistry
, the date range that the Calendar
should cover (a 'from' and 'to' date) and a limit if there is a maximum limit on the amount of events you want back.
$eventsRegistry = new EventRegistry();
$calendar = Plummer\Calendarful\Calendar\Calendar::create()
->populate($eventsRegistry, new \DateTime('2014-04-01'), new \DateTime('2014-04-30'));
As calendars implement the CalendarInterface
which in turn extends IteratorAggregate, they are traversable.
The default Calendar
uses an ArrayIterator which means we can access the events like so:
foreach($calendar as $event) {
// Use event as necessary...
}
Recurrent Events
To identify recurrent events and generate occurrences for them, a RecurrenceFactory
comes into the above process.
$eventsRegistry = new EventRegistry();
$recurrenceFactory = new \Plummer\Calendarful\Recurrence\RecurrenceFactory();
$recurrenceFactory->addRecurrenceType('daily', 'Plummer\Calendarful\Recurrence\Type\Daily');
$recurrenceFactory->addRecurrenceType('weekly', 'Plummer\Calendarful\Recurrence\Type\Weekly');
$recurrenceFactory->addRecurrenceType('monthly', 'Plummer\Calendarful\Recurrence\Type\MonthlyDate');
$calendar = Plummer\Calendarful\Calendar\Calendar::create($recurrenceFactory)
->populate($eventsRegistry, new \DateTime('2014-04-01'), new \DateTime('2014-04-30'));
We can see that the three default package recurrence types were injected into the RecurrenceFactory
and passed to the Calendar
.
In order for occurrences to be generated, the getRecurrenceType()
return value for a recurrent event should match up to the first parameter value from where Recurrence Types are added to the RecurrenceFactory
e.g. 'daily', 'weekly' or 'monthly' above.
When occurrences are generated, they will be a clone of their parent aside from updates to their date and recurrence related properties.
Overriding Occurrences
If you are using this package for its recurrence functionality, it is likely you will want to override an occurrence at some point.
For instance, you may have a weekly recurring event that runs every Monday but you may want next week's occurrence to run on the Tuesday instead and continue on Mondays again thereafter. This is where occurrence overrides come in.
When you want to override an occurrence you need to create a new Event
and save it to your storage method of choice.
For the override to be recognised by the package you need to update the values of those properties on the Event model
returned by getParentId()
and getOccurrenceDate
.
Lets say your Event
model has properties called parentId
and occurrenceDate
in conjunction with those EventInterface
methods mentioned.
To override next Monday's occurrence to Tuesday you would need to set the parentId
to the Id
value of the parent event that recurs every Monday, and the occurrenceDate
to the date that the occurrence would have been; the Monday. The startDate
would also need to be updated to the Tuesday's date. Once that new event is saved, the Monday occurrence next week would be replaced by the override in the Calendar
.
If a parent event start date ever changes, all of the occurrence dates for the overrides of the occurrences for that event need to be altered by the same difference in time to ensure the overrides still work.
Adding your own Recurrence Types
To add your own Recurrence Type all you need to do is create a new class that implements the RecurrenceInterface
and its methods.
The new Recurrence Type can then be added to the RecurrenceFactory
in the same way as shown above.
$recurrenceFactory = new \Plummer\Calendarful\Recurrence\RecurrenceFactory();
$recurrenceFactory->addRecurrenceType('ThisShouldMatchAnEventRecurrenceTypePropertyValue', 'Another\RecurrenceType\ClassPath');
Different Types of Calendars
This package supports different types of calendars as long as they implement the CalendarInterface
.
You may want to use multiple calendars at once, in which case you can use the CalendarFactory
.
You add calendars to the factory in much the same way as the RecurrenceFactory
works.
$calendarFactory = new \Plummer\Calendarful\Calendar\CalendarFactory();
$calendarFactory->addCalendarType('gregorian', 'Plummer\Calendarful\Calendar\Calendar');
$calendarFactory->addCalendarType('anotherType', 'Another\CalendarType\ClassPath');
Next, you can either retrieve the calendar type you desire.
$calendar = $calendarFactory->createCalendar('gregorian');
Or retrieve all calendar types to loop through etc.
foreach($calendarFactory->getCalendarTypes() as $type => $calendar) {
// Use calendar...
}
Extending the Package
There are interfaces for every component within this package therefore if the default implementations do not do exactly as you wish or you want them to work slightly differently it is quite simple to construct your own implementation. This may be for one component or for all.
If you do use your own components, I highly recommend looking at the functionality of the existing default components as you may wish to use parts e.g. to ensure occurrence overrides etc still function.
Testing
Unit tests can be run inside the package:
$ ./vendor/bin/phpunit
Contributing
Please see CONTRIBUTING for details.
License
plummer/calendarful is licensed under the MIT license. See the LICENSE
file for more details.