├── CHANGELOG.md ├── LICENSE.md ├── README-development.md ├── README-todo.md ├── README.md ├── composer.json ├── config └── acalendar.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ ├── create_acalendar_events_table.php │ └── create_acalendar_table.php.stub ├── docker-compose.yml ├── resources └── views │ └── .gitkeep └── src ├── ACalendar.php ├── ACalendarServiceProvider.php ├── Collections └── EventInstanceDTOCollection.php ├── Commands └── ACalendarCommand.php ├── Contracts └── EventableModelContract.php ├── DTOs └── EventInstanceDTO.php ├── Enums ├── CollectionBreakdown.php ├── RepeatFrequency.php └── Type.php ├── Exceptions ├── AEventParameterCompareException.php └── EventParameterValidationException.php ├── Facades └── ACalendar.php ├── Models ├── Event.php └── Eventable.php └── Traits └── HasEvents.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `ACalendar` will be documented in this file. 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) AuroraWebSoftware 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-development.md: -------------------------------------------------------------------------------- 1 | # Development Documentation 2 | ## Docker Compose 3 | 4 | ``` 5 | docker-compose up -d 6 | ``` 7 | 8 | ## Running Pest 9 | 10 | ``` 11 | composer test 12 | ``` 13 | 14 | ## Running Pint 15 | 16 | ``` 17 | ./vendor/bin/pint 18 | ``` 19 | 20 | 21 | ## Running Phpstan 22 | 23 | ``` 24 | ./vendor/bin/phpstan analyse 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /README-todo.md: -------------------------------------------------------------------------------- 1 | 2 | - Tüm modellerin eventlerini alan bir facade 3 | - config dosyasındaki gereklilik 4 | - dökümantasyon -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Laravel ACalendar Package** 2 | 3 | The Laravel ACalendar package is designed to enrich Laravel applications with advanced event management capabilities. It allows developers to seamlessly integrate event functionalities into Eloquent models, manage event occurrences, and handle repeating events with ease. This guide outlines the package's main features, installation process, and usage with detailed examples. 4 | 5 | ## **Features and Main Concepts** 6 | 7 | - **Flexible Event Management**: Create, update, and delete events directly associated with Eloquent models. 8 | - **Support for Various Event Types**: Handles different types of events, including single, all-day, ranged dates, and timed events. 9 | - **Repeating Events**: Comprehensive support for repeating events with customizable frequencies. 10 | - **Eloquent Model Integration**: Easy integration with any Eloquent model using a trait and interface. 11 | - **Dynamic Event Instances Generation**: Automatically handles the generation of event instances for repeating events within specified date ranges. 12 | - **Custom Event Collection Method**: Provides a **`byDay()`** method for grouping event instances, facilitating calendar views or daily summaries. 13 | 14 | ## **Installation** 15 | 16 | 1. Install the package via Composer: 17 | 18 | ```bash 19 | composer require aurorawebsoftware/acalendar 20 | 21 | ``` 22 | 23 | 1. Publish the configuration and migration files: 24 | 25 | ```bash 26 | php artisan vendor:publish --provider="AuroraWebSoftware\ACalendar\ACalendarServiceProvider" 27 | 28 | ``` 29 | 30 | 1. Execute the migrations: 31 | 32 | ```bash 33 | php artisan migrate 34 | 35 | ``` 36 | 37 | ## **Enums** 38 | 39 | ### **Type Enum** 40 | 41 | - **`DATE_ALL_DAY`**: Events occurring throughout the day. 42 | - **`DATE_POINT`**: Events assigned to a specific date. 43 | - **`DATETIME_POINT`**: Events assigned to a specific datetime. 44 | - **`DATE_RANGE`**: Events spanning across multiple dates. 45 | - **`DATETIME_RANGE`**: Events with a specific start and end datetime. 46 | 47 | ### **RepeatFrequency Enum** 48 | 49 | - **`DAY`**: Event repeats daily. 50 | - **`WEEK`**: Event repeats weekly. 51 | - **`MONTH`**: Event repeats monthly. 52 | - **`YEAR`**: Event repeats yearly. 53 | 54 | ## **Integration with Models** 55 | 56 | Implement the **`EventableModelContract`** and use the **`HasEvents`** trait within your model: 57 | 58 | ```php 59 | namespace App\Models; 60 | 61 | use AuroraWebSoftware\ACalendar\Contracts\EventableModelContract; 62 | use AuroraWebSoftware\ACalendar\Traits\HasEvents; 63 | use Illuminate\Database\Eloquent\Model; 64 | 65 | class Task extends Model implements EventableModelContract 66 | { 67 | use HasEvents; 68 | 69 | protected $fillable = ['name']; 70 | 71 | public static function getModelType(): string 72 | { 73 | return self::class; 74 | } 75 | 76 | public function getModelId(): int 77 | { 78 | return $this->id; 79 | } 80 | 81 | public function getEventTitle(): ?string 82 | { 83 | return $this->name; 84 | } 85 | } 86 | 87 | ``` 88 | 89 | 90 | 91 | ## **Usage Examples** 92 | 93 | ### **Creating Events** 94 | 95 | ```php 96 | $task = Task::find(1); 97 | $task->updateOrCreateEvent( 98 | key: 'deadline', 99 | type: Type::DATE_POINT, 100 | start: Carbon::tomorrow(), 101 | title: 'Preparing SRS Docs' 102 | ); 103 | 104 | $task->updateOrCreateEvent( 105 | key: 'deadline', 106 | type: Type::DATE_POINT, 107 | start: Carbon::tomorrow(), 108 | title: 'Preparing SRS Docs' 109 | ); 110 | 111 | ``` 112 | 113 | > Only one event can be created for a model with a key 114 | 115 | ### **Retrieving Event Instances** 116 | 117 | - Dynamic method on an instance: 118 | 119 | ```php 120 | $events = $task->eventInstances('deadline', Carbon::now(), Carbon::now()->addMonth(1)); 121 | ``` 122 | 123 | - Static method on the model class: 124 | 125 | ```php 126 | $events = Task::allEventInstances('deadline', Carbon::now(), Carbon::now()->addMonth(1)); 127 | ``` 128 | 129 | ### **Handling Repeating Events** 130 | 131 | ```php 132 | $meeting = Meeting::find(1); 133 | $meeting->updateOrCreateEvent( 134 | key: 'Weekly Review', 135 | type: Type::DATETIME_POINT, 136 | start: Carbon::parse('next monday 10:00'), 137 | repeatFrequency: RepeatFrequency::WEEK, 138 | repeatPeriod: 1, 139 | title: 'Weekly Review Meeting' 140 | ); 141 | 142 | ``` 143 | 144 | ### **Using `byDay` Method** 145 | 146 | The byDay() method in the Laravel ACalendar package groups event instances by their occurrence date, returning a collection where each key is a date and the value is a collection of events happening on that date. This method simplifies creating calendar views or daily schedules by organizing events in a date-indexed format, making it straightforward to display what events are happening on each day. 147 | 148 | - Facilitates the development of calendar interfaces by categorizing events by day. 149 | 150 | ```php 151 | $eventInstances = $meeting->eventInstances(null, Carbon::now(), Carbon::now()->addWeeks(4)); 152 | $byDay = $eventInstances->byDay(); 153 | 154 | foreach ($byDay as $date => $events) { 155 | echo "Date: $date\n"; 156 | foreach ($events as $event) { 157 | echo "- {$event->title} at {$event->start->format('H:i')}\n"; 158 | } 159 | } 160 | 161 | ``` 162 | 163 | 164 | 165 | 166 | 167 | 168 | ## **Scenario Setup** 169 | 170 | Assuming we have three models - **`Conference`**, **`Webinar`**, and **`Exhibition`**, each integrated with the ACalendar package as shown in previous examples. These models will demonstrate different event types, such as **`DATE_ALL_DAY`**, **`DATE_RANGE`**, and **`DATETIME_RANGE`**. 171 | 172 | ### **Conference: All-Day Event** 173 | 174 | Conferences often last the entire day. Here's how you might set up an all-day event for a conference: 175 | 176 | ```php 177 | $conference = Conference::create(['name' => 'Tech Innovators Conference', 'description' => 'A gathering of technology innovators.']); 178 | 179 | $conference->updateOrCreateEvent( 180 | key: 'tech_innovators_2024', 181 | type: Type::DATE_ALL_DAY, 182 | start: Carbon::parse('2024-09-10'), 183 | title: 'Tech Innovators Conference - All Day' 184 | ); 185 | 186 | ``` 187 | 188 | ### **Webinar: Date Range Event** 189 | 190 | Webinars can span multiple days. This example demonstrates creating an event that covers a range of dates: 191 | 192 | ```php 193 | $webinar = Webinar::create(['title' => 'Digital Marketing 101', 'host' => 'Marketing Gurus']); 194 | 195 | $webinar->updateOrCreateEvent( 196 | key: 'digital_marketing_101', 197 | type: Type::DATE_RANGE, 198 | start: Carbon::parse('2024-10-05'), 199 | end: Carbon::parse('2024-10-07'), 200 | title: 'Digital Marketing 101 Webinar' 201 | ); 202 | 203 | ``` 204 | 205 | ### **Exhibition: DateTime Range Event** 206 | 207 | Exhibitions may have specific start and end times. Here's how you'd set up an event with a datetime range: 208 | 209 | ```php 210 | $exhibition = Exhibition::create(['name' => 'Artists of the 21st Century', 'location' => 'City Art Gallery']); 211 | 212 | $exhibition->updateOrCreateEvent( 213 | key: '21st_century_artists', 214 | type: Type::DATETIME_RANGE, 215 | start: Carbon::parse('2024-11-20 09:00'), 216 | end: Carbon::parse('2024-11-20 17:00'), 217 | title: 'Artists of the 21st Century Exhibition' 218 | ); 219 | 220 | ``` 221 | 222 | ## **Querying and Displaying Event Instances** 223 | 224 | ### **Displaying Upcoming Conferences** 225 | 226 | Retrieve and display all upcoming conferences for the next year: 227 | 228 | ```php 229 | phpCopy code 230 | $upcomingConferences = Conference::allEventInstances( 231 | null, 232 | Carbon::now(), 233 | Carbon::now()->addYear(1) 234 | ); 235 | 236 | foreach ($upcomingConferences as $event) { 237 | echo "Conference: {$event->title} on {$event->start->toDateString()}\n"; 238 | } 239 | 240 | ``` 241 | 242 | ### **Webinar Schedule for the Next Month** 243 | 244 | Generate a schedule of all webinars happening in the next month, grouped by day: 245 | 246 | ```php 247 | phpCopy code 248 | $nextMonthWebinars = Webinar::allEventInstances( 249 | null, 250 | Carbon::now()->addMonth(), 251 | Carbon::now()->addMonths(2) 252 | )->byDay(); 253 | 254 | foreach ($nextMonthWebinars as $date => $webinars) { 255 | echo "Date: $date\n"; 256 | foreach ($webinars as $webinar) { 257 | echo "- Webinar: {$webinar->title} from {$webinar->start->toDateString()} to {$webinar->end->toDateString()}\n"; 258 | } 259 | } 260 | 261 | ``` 262 | 263 | ### **Exhibition Hours** 264 | 265 | For exhibitions, it might be useful to know the exact opening and closing times: 266 | 267 | ```php 268 | phpCopy code 269 | $exhibitionDetails = Exhibition::allEventInstances('21st_century_artists', Carbon::now(), Carbon::now()->addMonth(1)); 270 | 271 | foreach ($exhibitionDetails as $detail) { 272 | echo "Exhibition: {$detail->title}, Start: {$detail->start->toDateTimeString()}, End: {$detail->end->toDateTimeString()}\n"; 273 | } 274 | 275 | ``` 276 | 277 | These examples illustrate just a few of the many possibilities enabled by the Laravel ACalendar package for managing events. By leveraging different event types and repeat frequencies, developers can tailor the package to meet a wide array of event management needs within their Laravel applications. 278 | 279 | 280 | 281 | 282 | 283 | 284 | This Laravel ACalendar package guide aims to provide a solid foundation for integrating and utilizing event management within your Laravel applications. By following the installation instructions and exploring the comprehensive examples, you can leverage the package's functionalities to enhance your projects with sophisticated event handling capabilities. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurorawebsoftware/acalendar", 3 | "description": "This is my package acalendar", 4 | "keywords": [ 5 | "AuroraWebSoftware", 6 | "laravel", 7 | "acalendar" 8 | ], 9 | "homepage": "https://github.com/aurorawebsoftware/acalendar", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Aurora Web Software Team", 14 | "email": "websoftwareteam@aurorabilisim.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2|^8.3", 20 | "spatie/laravel-package-tools": "^1.9.2", 21 | "laravel/framework": "^12.0" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.0", 25 | "nunomaduro/collision": "^8", 26 | "nunomaduro/larastan": "^3", 27 | "orchestra/testbench": "^10.0", 28 | "pestphp/pest": "^3.0", 29 | "pestphp/pest-plugin-arch": "^3.0", 30 | "pestphp/pest-plugin-laravel": "^3.0", 31 | "phpstan/extension-installer": "^1.1", 32 | "phpstan/phpstan-deprecation-rules": "^2.0", 33 | "phpstan/phpstan-phpunit": "^2.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "AuroraWebSoftware\\ACalendar\\": "src/", 38 | "AuroraWebSoftware\\ACalendar\\Database\\Factories\\": "database/factories/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "AuroraWebSoftware\\ACalendar\\Tests\\": "tests/" 44 | } 45 | }, 46 | "scripts": { 47 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 48 | "analyse": "vendor/bin/phpstan analyse", 49 | "test": "vendor/bin/pest", 50 | "test-coverage": "vendor/bin/pest --coverage", 51 | "format": "vendor/bin/pint" 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "allow-plugins": { 56 | "pestphp/pest-plugin": true, 57 | "phpstan/extension-installer": true 58 | } 59 | }, 60 | "extra": { 61 | "laravel": { 62 | "providers": [ 63 | "AuroraWebSoftware\\ACalendar\\ACalendarServiceProvider" 64 | ], 65 | "aliases": { 66 | "ACalendar": "AuroraWebSoftware\\ACalendar\\Facades\\ACalendar" 67 | } 68 | } 69 | }, 70 | "minimum-stability": "dev", 71 | "prefer-stable": true 72 | } 73 | -------------------------------------------------------------------------------- /config/acalendar.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'all_day_event_start_time' => '09:00', 8 | 'all_day_event_end_time' => '22:00', 9 | 'date_event_duration_min' => 30, 10 | 'datetime_event_duration_min' => 30, 11 | ], 12 | 13 | ]; 14 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('model_type')->nullable()->index(); 14 | $table->unsignedInteger('model_id')->nullable()->index(); 15 | $table->string('key')->nullable(false)->index(); 16 | $table->enum('type', 17 | ['date_point', 'datetime_point', 'date_all_day', 'date_range', 'datetime_range'] 18 | )->default('date_point')->nullable(false)->index(); 19 | $table->string('title')->nullable(false); 20 | $table->date('start_date')->nullable()->index(); 21 | $table->date('end_date')->nullable()->index(); 22 | $table->dateTime('start_datetime')->nullable()->index(); 23 | $table->dateTime('end_datetime')->nullable()->index(); 24 | $table->string('repeat_frequency')->nullable()->index(); 25 | $table->unsignedInteger('repeat_period')->nullable()->index(); 26 | $table->dateTime('repeat_until')->nullable()->index(); 27 | $table->timestamps(); 28 | 29 | $table->unique(['key', 'model_type', 'model_id']); 30 | }); 31 | } 32 | 33 | public function down() 34 | { 35 | Schema::drop('acalendar_events'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/create_acalendar_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mariadb: 5 | image: mariadb:10.8 6 | ports: 7 | - "33063:3306" 8 | volumes: 9 | - ~/apps/acalendar/mariadb:/var/lib/mysql 10 | environment: 11 | - MYSQL_ROOT_PASSWORD=acalendar 12 | - MYSQL_PASSWORD=acalendar 13 | - MYSQL_USER=acalendar 14 | - MYSQL_DATABASE=acalendar 15 | networks: 16 | default: 17 | driver: bridge 18 | ipam: 19 | config: 20 | - subnet: 172.15.57.0/24 21 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AuroraWebSoftware/ACalendar/b01d3747abd399fad1fdbec31b7195ec08b83bca/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/ACalendar.php: -------------------------------------------------------------------------------- 1 | name('acalendar') 20 | ->hasConfigFile(); 21 | //->hasViews() 22 | //->hasCommand(ACalendarCommand::class); 23 | //->hasMigration('create_acalendar_events_table'); 24 | } 25 | 26 | public function boot(): void 27 | { 28 | parent::boot(); 29 | // load packages migrations 30 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 31 | $this->publishes([ 32 | __DIR__.'/../config' => config_path(), 33 | ], 'acalendar-config'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Collections/EventInstanceDTOCollection.php: -------------------------------------------------------------------------------- 1 | each(function (EventInstanceDTO $eventInstanceDTO) use ($byDayCollection) { 19 | 20 | $period = CarbonPeriod::create($eventInstanceDTO->start, $eventInstanceDTO->end ?? $eventInstanceDTO->start); 21 | 22 | foreach ($period as $item) { 23 | 24 | $key = $item->format('Y-m-d'); 25 | 26 | if ($byDayCollection->get($key) == null) { 27 | $byDayCollection->put($key, collect()); 28 | } 29 | $byDayCollection->get($key)->push($eventInstanceDTO); 30 | } 31 | }); 32 | 33 | return $byDayCollection; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Commands/ACalendarCommand.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | 17 | return self::SUCCESS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Contracts/EventableModelContract.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | public function event(string $key): Event|Builder; 37 | 38 | /** 39 | * returns the events of the model with the given keys with polymorphic relation 40 | * returns all if $key is null 41 | * 42 | * @return Event|Builder 43 | */ 44 | public function events(?array $key = null): Event|Builder; 45 | 46 | public function deleteEvent(string $key): void; 47 | 48 | /** 49 | * gives all events and recurring occurrences between $start and $end and given keys for a model instance with polymorphic relation 50 | * 51 | * @return EventInstanceDTOCollection 52 | */ 53 | public function eventInstances( 54 | array|string|null $keyOrKeys, 55 | Carbon $start, 56 | Carbon $end, 57 | ): EventInstanceDTOCollection; 58 | 59 | /** 60 | * gives all events and recurring occurrences between $start and $end and given keys for a model instance with polymorphic relation 61 | * 62 | * @return EventInstanceDTOCollection 63 | */ 64 | public function scopeAllEventInstances( 65 | Builder $query, 66 | array|string|null $keyOrKeys, 67 | Carbon $start, 68 | Carbon $end, 69 | ): EventInstanceDTOCollection; 70 | 71 | public function scopeAuthorized(Builder $query): void; 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function getEventMetadata(): array; 77 | } 78 | -------------------------------------------------------------------------------- /src/DTOs/EventInstanceDTO.php: -------------------------------------------------------------------------------- 1 | code = $code; 43 | $this->modelType = $modelType; 44 | $this->modelId = $modelId; 45 | $this->key = $key; 46 | $this->type = $type; 47 | $this->title = $title; 48 | $this->start = $start; 49 | $this->end = $end; 50 | } 51 | 52 | public function calendarStartDatetimePoint(): Carbon 53 | { 54 | return $this->start; 55 | } 56 | 57 | public function calendarEndDatetimePoint(): Carbon 58 | { 59 | return $this->end; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Enums/CollectionBreakdown.php: -------------------------------------------------------------------------------- 1 | 'datetime', 41 | 'start_datetime' => 'datetime', 42 | 'end_datetime' => 'datetime', 43 | 'start_date' => 'datetime', 44 | 'end_date' => 'datetime', 45 | 'type' => Type::class, 46 | 'repeat_frequency' => RepeatFrequency::class, 47 | ]; 48 | 49 | protected $fillable = 50 | ['key', 'type', 'repeat_frequency', 'repeat_period', 'repeat_until', 'model_type', 'model_id', 51 | 'title', 'all_day', 'start_date', 'end_date', 'start_datetime', 'end_datetime']; 52 | 53 | /** 54 | * @throws Exception 55 | */ 56 | public function getStartAttribute(): Carbon 57 | { 58 | if ($this->type == Type::DATE_ALL_DAY) { 59 | return $this->start_date; 60 | } elseif ($this->type == Type::DATE_POINT) { 61 | return $this->start_date; 62 | } elseif ($this->type == Type::DATETIME_POINT) { 63 | return $this->start_datetime; 64 | } elseif ($this->type == Type::DATE_RANGE) { 65 | return $this->start_date; 66 | } elseif ($this->type == Type::DATETIME_RANGE) { 67 | return $this->start_datetime; 68 | } else { 69 | throw new Exception('Invalid Event Type'); 70 | } 71 | } 72 | 73 | /** 74 | * @throws Exception 75 | */ 76 | public function getEndAttribute(): ?Carbon 77 | { 78 | if ($this->type == Type::DATE_ALL_DAY) { 79 | return null; 80 | } elseif ($this->type == Type::DATE_POINT) { 81 | return null; 82 | } elseif ($this->type == Type::DATETIME_POINT) { 83 | return null; 84 | } elseif ($this->type == Type::DATE_RANGE) { 85 | return $this->end_date; 86 | } elseif ($this->type == Type::DATETIME_RANGE) { 87 | return $this->end_datetime; 88 | } else { 89 | throw new Exception('Invalid Event Type'); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Models/Eventable.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | 32 | public function getModelName(): ?string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function getEventTitle(): ?string 38 | { 39 | return $this->name; 40 | } 41 | 42 | public function scopeAuthorized(Builder $query): void 43 | { 44 | $query->where('name', '=', 'event701'); 45 | } 46 | 47 | public function getEventMetadata(): array 48 | { 49 | return ['key' => $this->name]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Traits/HasEvents.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d'); 39 | } elseif ($type == Type::DATE_POINT) { 40 | $data['start_date'] = $start->format('Y-m-d'); 41 | } elseif ($type == Type::DATETIME_POINT) { 42 | $data['start_datetime'] = $start->format('Y-m-d H:i:s'); 43 | } elseif ($type == Type::DATE_RANGE) { 44 | 45 | if (! $end) { 46 | throw new EventParameterValidationException('end is missing.'); 47 | } 48 | 49 | if ($start->gt($end)) { 50 | throw new EventParameterValidationException('start is greater than end.'); 51 | } 52 | 53 | $data['start_date'] = $start->format('Y-m-d'); 54 | $data['end_date'] = $end->format('Y-m-d'); 55 | } elseif ($type == Type::DATETIME_RANGE) { 56 | 57 | if (! $end) { 58 | throw new EventParameterValidationException('end is missing.'); 59 | } 60 | 61 | if ($start->gt($end)) { 62 | throw new EventParameterValidationException('start is greater than end.'); 63 | } 64 | 65 | $data['start_datetime'] = $start->format('Y-m-d H:i:s'); 66 | $data['end_datetime'] = $end->format('Y-m-d H:i:s'); 67 | } 68 | 69 | if ($repeatFrequency && ! $repeatPeriod) { 70 | throw new EventParameterValidationException('repeatPeriod is missing.'); 71 | } 72 | 73 | $data['repeat_frequency'] = $repeatFrequency?->value; 74 | $data['repeat_period'] = $repeatPeriod; 75 | 76 | if ($repeatUntil) { 77 | $data['repeat_until'] = $repeatUntil->format('Y-m-d H:i:s'); 78 | } 79 | 80 | Event::query()->updateOrCreate( 81 | [ 82 | 'key' => $key, 83 | 'model_type' => self::getModelType(), 84 | 'model_id' => $this->getModelId(), 85 | ], 86 | [ 87 | 'key' => $key, 88 | 'type' => $type, 89 | 'model_type' => self::getModelType(), 90 | 'model_id' => $this->getModelId(), 91 | 'title' => $this->getEventTitle(), 92 | ...$data, 93 | ] 94 | ); 95 | 96 | return Event::query() 97 | ->where('key', $key) 98 | ->where('model_type', self::getModelType()) 99 | ->where('model_id', $this->getModelId()) 100 | ->first(); 101 | } 102 | 103 | /** 104 | * @return Event|Builder 105 | */ 106 | public function event(string $key): Event|Builder 107 | { 108 | return Event::query()->where('key', $key) 109 | ->where('model_type', self::getModelType()) 110 | ->where('model_id', $this->getModelId()); 111 | } 112 | 113 | /** 114 | * @param array|null $key 115 | * @return Event|Builder 116 | */ 117 | public function events(?array $key = null): Event|Builder 118 | { 119 | if (! $key) { 120 | return Event::query() 121 | ->where('model_type', self::getModelType()) 122 | ->where('model_id', $this->getModelId()); 123 | } 124 | 125 | return Event::query()->whereIn('key', $key) 126 | ->where('model_type', self::getModelType()) 127 | ->where('model_id', $this->getModelId()); 128 | } 129 | 130 | public function deleteEvent(string $key): void 131 | { 132 | $this->event($key)->delete(); 133 | } 134 | 135 | /** 136 | * Events and recurring occurrences between $start and $end and given keys for a model instance 137 | * 138 | * @throws Exception 139 | */ 140 | public function eventInstances( 141 | array|string|null $keyOrKeys, 142 | \Illuminate\Support\Carbon $start, 143 | \Illuminate\Support\Carbon $end 144 | ): EventInstanceDTOCollection { 145 | if (is_string($keyOrKeys)) { 146 | $keyOrKeys = [$keyOrKeys]; 147 | } 148 | 149 | $events = $this->events($keyOrKeys) 150 | ->where(function (Builder $q) use ($start, $end) { 151 | $q 152 | ->where('start_date', '>=', $start->format('Y-m-d')) 153 | ->orWhere('start_datetime', '>=', $start->format('Y-m-d H:i:s')) 154 | ->orWhere('repeat_until', '<=', $end->format('Y-m-d H:i:s')) 155 | ->orWhere(function (Builder $q) { 156 | $q->whereNotNull('repeat_frequency')->whereNull('repeat_until'); 157 | }); 158 | }) 159 | ->where(function (Builder $q) use ($end) { 160 | $q 161 | ->where('end_date', '<=', $end->format('Y-m-d')) 162 | ->orWhere('end_datetime', '<=', $end->format('Y-m-d H:i:s')) 163 | ->orWhere('repeat_until', '<=', $end->format('Y-m-d H:i:s')) 164 | ->orWhere(function (Builder $q) { 165 | $q->whereNotNull('repeat_frequency')->whereNull('repeat_until'); 166 | }) 167 | ->orWhere(function (Builder $q) use ($end) { 168 | $q->whereNull('end_date')->where('start_date', '<=', $end->format('Y-m-d')); 169 | }) 170 | ->orWhere(function (Builder $q) use ($end) { 171 | $q->whereNull('end_datetime')->where('start_datetime', '<=', $end->format('Y-m-d H:i:s')); 172 | }); 173 | }) 174 | ->orderBy('start_date') 175 | ->orderBy('start_datetime') 176 | ->get(); 177 | 178 | $eventInstanceDTOCollection = EventInstanceDTOCollection::make(); 179 | 180 | // loop all event models 181 | foreach ($events as $event) { 182 | 183 | // for not repeating event, return one event instance dto only 184 | if ($event->repeat_frequency == null) { 185 | 186 | $eventInstanceDTO = new EventInstanceDTO( 187 | code: $event->id.'_'.$event->key.'_'.$event->start->format('Y-m-d H:i:s'), 188 | modelType: $event->model_type, 189 | modelId: $event->model_id, 190 | key: $event->key, 191 | type: $event->type, 192 | title: $event->title, 193 | start: $event->start, 194 | end: $event->end 195 | ); 196 | 197 | $eventInstanceDTOCollection->push($eventInstanceDTO); 198 | 199 | } else { 200 | 201 | $repeatFreqAdditionDay = 0; 202 | if ($event->repeat_frequency == RepeatFrequency::DAY) { 203 | $repeatFreqAdditionDay = 1 * $event->repeat_period; 204 | } elseif ($event->repeat_frequency == RepeatFrequency::WEEK) { 205 | $repeatFreqAdditionDay = 7 * $event->repeat_period; 206 | } elseif ($event->repeat_frequency == RepeatFrequency::MONTH) { 207 | throw new Exception('not implemented yet'); 208 | } elseif ($event->repeat_frequency == RepeatFrequency::YEAR) { 209 | throw new Exception('not implemented yet'); 210 | } 211 | 212 | $instanceStartDate = $event->start_date; 213 | $instanceStartDatetime = $event->start_datetime; 214 | $instanceEndDate = $event->end_date; 215 | $instanceEndDatetime = $event->end_datetime; 216 | 217 | while (true) { 218 | 219 | $dtoCreation = true; 220 | 221 | if ($instanceStartDate && $instanceStartDate->lt($start)) { 222 | $dtoCreation = false; 223 | } 224 | 225 | if ($instanceStartDatetime && $instanceStartDatetime->lt($start)) { 226 | $dtoCreation = false; 227 | } 228 | 229 | if ($instanceStartDate && 230 | ($instanceStartDate->gt($end) || 231 | ($event->repeat_until && $instanceStartDate->gt($event->repeat_until)) 232 | ) 233 | ) { 234 | break; 235 | } 236 | 237 | if ($instanceStartDatetime && 238 | ($instanceStartDatetime->gt($end) || 239 | ($event->repeat_until && $instanceStartDatetime->gt($event->repeat_until)) 240 | ) 241 | ) { 242 | break; 243 | } 244 | 245 | if ($dtoCreation) { 246 | 247 | $code = $event->id.'_'.$event->key.'_'. 248 | $instanceStartDate?->format('Y-m-d H:i:s'). 249 | $instanceStartDatetime?->format('Y-m-d H:i:s'); 250 | 251 | $eventInstanceDTO = new EventInstanceDTO( 252 | code: $code, 253 | modelType: $event->model_type, 254 | modelId: $event->model_id, 255 | key: $event->key, 256 | type: $event->type, 257 | title: $event->title, 258 | start: $instanceStartDate?->clone() ?? $instanceStartDatetime?->clone(), 259 | end: $instanceEndDate?->clone() ?? $instanceEndDatetime?->clone() ?? null 260 | ); 261 | 262 | $eventInstanceDTOCollection->push($eventInstanceDTO); 263 | } 264 | 265 | if ($instanceStartDate) { 266 | $instanceStartDate->addDays($repeatFreqAdditionDay); 267 | } 268 | 269 | if ($instanceStartDatetime) { 270 | $instanceStartDatetime->addDays($repeatFreqAdditionDay); 271 | } 272 | 273 | if ($instanceEndDate) { 274 | $instanceEndDate->addDays($repeatFreqAdditionDay); 275 | } 276 | 277 | if ($instanceEndDatetime) { 278 | $instanceEndDatetime->addDays($repeatFreqAdditionDay); 279 | } 280 | 281 | } 282 | } 283 | } 284 | 285 | return $eventInstanceDTOCollection; 286 | } 287 | 288 | public function scopeAllEventInstances( 289 | Builder $query, array|string|null $keyOrKeys, 290 | \Illuminate\Support\Carbon $start, 291 | \Illuminate\Support\Carbon $end 292 | ): EventInstanceDTOCollection { 293 | 294 | if (is_string($keyOrKeys)) { 295 | $keyOrKeys = [$keyOrKeys]; 296 | } 297 | 298 | $eventBuilder = null; 299 | 300 | if (! $keyOrKeys) { 301 | $eventBuilder = Event::query() 302 | ->where('model_type', self::getModelType()); 303 | } else { 304 | $eventBuilder = Event::query() 305 | ->whereIn('key', $keyOrKeys) 306 | ->where('model_type', self::getModelType()); 307 | } 308 | 309 | $eventableIdsToBeFiltered = $query->pluck('id'); 310 | $eventBuilder->whereIn('model_id', $eventableIdsToBeFiltered); 311 | 312 | $events = $eventBuilder 313 | ->where(function (Builder $q) use ($start, $end) { 314 | $q 315 | ->where('start_date', '>=', $start->format('Y-m-d')) 316 | ->orWhere('start_datetime', '>=', $start->format('Y-m-d H:i:s')) 317 | ->orWhere('repeat_until', '<=', $end->format('Y-m-d H:i:s')) 318 | ->orWhere(function (Builder $q) { 319 | $q->whereNotNull('repeat_frequency')->whereNull('repeat_until'); 320 | }); 321 | }) 322 | ->where(function (Builder $q) use ($end) { 323 | $q 324 | ->where('end_date', '<=', $end->format('Y-m-d')) 325 | ->orWhere('end_datetime', '<=', $end->format('Y-m-d H:i:s')) 326 | ->orWhere('repeat_until', '<=', $end->format('Y-m-d H:i:s')) 327 | ->orWhere(function (Builder $q) { 328 | $q->whereNotNull('repeat_frequency')->whereNull('repeat_until'); 329 | }) 330 | ->orWhere(function (Builder $q) use ($end) { 331 | $q->whereNull('end_date')->where('start_date', '<=', $end->format('Y-m-d')); 332 | }) 333 | ->orWhere(function (Builder $q) use ($end) { 334 | $q->whereNull('end_datetime')->where('start_datetime', '<=', $end->format('Y-m-d H:i:s')); 335 | }); 336 | }) 337 | ->orderBy('start_date', 'asc') 338 | ->orderBy('start_datetime', 'asc') 339 | ->get(); 340 | 341 | $eventInstanceDTOCollection = EventInstanceDTOCollection::make(); 342 | 343 | // loop all event models 344 | foreach ($events as $event) { 345 | 346 | // for not repeating event, return one event instance dto only 347 | if ($event->repeat_frequency == null) { 348 | 349 | $eventInstanceDTO = new EventInstanceDTO( 350 | code: $event->id.'_'.$event->key.'_'.$event->start->format('Y-m-d H:i:s'), 351 | modelType: $event->model_type, 352 | modelId: $event->model_id, 353 | key: $event->key, 354 | type: $event->type, 355 | title: $event->title, 356 | start: $event->start, 357 | end: $event->end 358 | ); 359 | 360 | $eventInstanceDTOCollection->push($eventInstanceDTO); 361 | 362 | } else { 363 | 364 | $repeatFreqAdditionDay = 0; 365 | if ($event->repeat_frequency == RepeatFrequency::DAY) { 366 | $repeatFreqAdditionDay = 1 * $event->repeat_period; 367 | } elseif ($event->repeat_frequency == RepeatFrequency::WEEK) { 368 | $repeatFreqAdditionDay = 7 * $event->repeat_period; 369 | } elseif ($event->repeat_frequency == RepeatFrequency::MONTH) { 370 | throw new Exception('not implemented yet'); 371 | } elseif ($event->repeat_frequency == RepeatFrequency::YEAR) { 372 | throw new Exception('not implemented yet'); 373 | } 374 | 375 | $instanceStartDate = $event->start_date; 376 | $instanceStartDatetime = $event->start_datetime; 377 | $instanceEndDate = $event->end_date; 378 | $instanceEndDatetime = $event->end_datetime; 379 | 380 | while (true) { 381 | 382 | $dtoCreation = true; 383 | 384 | if ($instanceStartDate && $instanceStartDate->lt($start)) { 385 | $dtoCreation = false; 386 | } 387 | 388 | if ($instanceStartDatetime && $instanceStartDatetime->lt($start)) { 389 | $dtoCreation = false; 390 | } 391 | 392 | if ($instanceStartDate && 393 | ($instanceStartDate->gt($end) || 394 | ($event->repeat_until && $instanceStartDate->gt($event->repeat_until)) 395 | ) 396 | ) { 397 | break; 398 | } 399 | 400 | if ($instanceStartDatetime && 401 | ($instanceStartDatetime->gt($end) || 402 | ($event->repeat_until && $instanceStartDatetime->gt($event->repeat_until)) 403 | ) 404 | ) { 405 | break; 406 | } 407 | 408 | if ($dtoCreation) { 409 | 410 | $code = $event->id.'_'.$event->key.'_'. 411 | $instanceStartDate?->format('Y-m-d H:i:s'). 412 | $instanceStartDatetime?->format('Y-m-d H:i:s'); 413 | 414 | $eventInstanceDTO = new EventInstanceDTO( 415 | code: $code, 416 | modelType: $event->model_type, 417 | modelId: $event->model_id, 418 | key: $event->key, 419 | type: $event->type, 420 | title: $event->title, 421 | start: $instanceStartDate?->clone() ?? $instanceStartDatetime?->clone(), 422 | end: $instanceEndDate?->clone() ?? $instanceEndDatetime?->clone() ?? null 423 | ); 424 | 425 | $eventInstanceDTOCollection->push($eventInstanceDTO); 426 | } 427 | 428 | if ($instanceStartDate) { 429 | $instanceStartDate->addDays($repeatFreqAdditionDay); 430 | } 431 | 432 | if ($instanceStartDatetime) { 433 | $instanceStartDatetime->addDays($repeatFreqAdditionDay); 434 | } 435 | 436 | if ($instanceEndDate) { 437 | $instanceEndDate->addDays($repeatFreqAdditionDay); 438 | } 439 | 440 | if ($instanceEndDatetime) { 441 | $instanceEndDatetime->addDays($repeatFreqAdditionDay); 442 | } 443 | 444 | } 445 | } 446 | 447 | } 448 | 449 | return $eventInstanceDTOCollection; 450 | } 451 | 452 | public function scopeAuthorized(Builder $query): void {} 453 | 454 | public function getEventMetadata(): array 455 | { 456 | return ['key' => 'value']; 457 | } 458 | } 459 | --------------------------------------------------------------------------------