├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── composer.json ├── docs └── README.md ├── phpcs.xml ├── phpstan.neon ├── src ├── CalendarPlugin.php ├── Controller │ └── Component │ │ └── CalendarComponent.php ├── Model │ └── Behavior │ │ └── CalendarBehavior.php └── View │ ├── Helper │ ├── CalendarHelper.php │ └── GoogleCalendarHelper.php │ └── IcalView.php └── tests ├── Fixture └── EventsFixture.php ├── TestCase ├── Controller │ └── Component │ │ └── CalendarComponentTest.php ├── Model │ └── Behavior │ │ └── CalendarBehaviorTest.php └── View │ ├── Helper │ ├── CalendarHelperTest.php │ └── GoogleCalendarHelperTest.php │ └── IcalViewTest.php ├── bootstrap.php ├── config └── routes.php └── schema.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | testsuite: 10 | runs-on: ubuntu-22.04 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php-version: ['8.1', '8.4'] 15 | db-type: [sqlite, mysql, pgsql] 16 | prefer-lowest: [''] 17 | include: 18 | - php-version: '8.1' 19 | db-type: 'sqlite' 20 | prefer-lowest: 'prefer-lowest' 21 | 22 | services: 23 | postgres: 24 | image: postgres 25 | ports: 26 | - 5432:5432 27 | env: 28 | POSTGRES_PASSWORD: postgres 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Setup Service 34 | if: matrix.db-type == 'mysql' 35 | run: | 36 | sudo service mysql start 37 | mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE cakephp;' 38 | - name: Setup PHP 39 | uses: shivammathur/setup-php@v2 40 | with: 41 | php-version: ${{ matrix.php-version }} 42 | extensions: mbstring, intl, pdo_${{ matrix.db-type }} 43 | coverage: pcov 44 | 45 | - name: Get composer cache directory 46 | id: composercache 47 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 48 | 49 | - name: Cache dependencies 50 | uses: actions/cache@v4 51 | with: 52 | path: ${{ steps.composercache.outputs.dir }} 53 | key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} 54 | 55 | - name: Composer install 56 | run: | 57 | composer --version 58 | if ${{ matrix.prefer-lowest == 'prefer-lowest' }} 59 | then 60 | composer update --prefer-lowest --prefer-stable 61 | composer require --dev dereuromark/composer-prefer-lowest:dev-master 62 | else 63 | composer install --no-progress --prefer-dist --optimize-autoloader 64 | fi 65 | - name: Run PHPUnit 66 | run: | 67 | if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi 68 | if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi 69 | if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi 70 | if [[ ${{ matrix.php-version }} == '8.1' ]]; then 71 | vendor/bin/phpunit --coverage-clover=coverage.xml 72 | else 73 | vendor/bin/phpunit 74 | fi 75 | - name: Validate prefer-lowest 76 | if: matrix.prefer-lowest == 'prefer-lowest' 77 | run: vendor/bin/validate-prefer-lowest -m 78 | 79 | - name: Upload coverage reports to Codecov 80 | if: success() && matrix.php-version == '8.1' 81 | uses: codecov/codecov-action@v4 82 | with: 83 | token: ${{ secrets.CODECOV_TOKEN }} 84 | 85 | validation: 86 | name: Coding Standard & Static Analysis 87 | runs-on: ubuntu-22.04 88 | 89 | steps: 90 | - uses: actions/checkout@v4 91 | 92 | - name: Setup PHP 93 | uses: shivammathur/setup-php@v2 94 | with: 95 | php-version: '8.1' 96 | extensions: mbstring, intl 97 | coverage: none 98 | 99 | - name: Get composer cache directory 100 | id: composercache 101 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 102 | 103 | - name: Cache dependencies 104 | uses: actions/cache@v4 105 | with: 106 | path: ${{ steps.composercache.outputs.dir }} 107 | key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} 108 | 109 | - name: Composer Install 110 | run: composer stan-setup 111 | 112 | - name: Run phpstan 113 | run: composer stan 114 | 115 | - name: Run phpcs 116 | run: composer cs-check 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mark Scherer 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.md: -------------------------------------------------------------------------------- 1 | # CakePHP Calendar plugin 2 | 3 | [![CI](https://github.com/dereuromark/cakephp-calendar/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dereuromark/cakephp-calendar/actions/workflows/ci.yml?query=branch%3Amaster) 4 | [![Coverage Status](https://codecov.io/gh/dereuromark/cakephp-calendar/branch/master/graph/badge.svg)](https://codecov.io/gh/dereuromark/cakephp-calendar) 5 | [![Latest Stable Version](https://poser.pugx.org/dereuromark/cakephp-calendar/v/stable.svg)](https://packagist.org/packages/dereuromark/cakephp-calendar) 6 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/) 7 | [![License](https://poser.pugx.org/dereuromark/cakephp-calendar/license.svg)](LICENSE) 8 | [![Total Downloads](https://poser.pugx.org/dereuromark/cakephp-calendar/d/total.svg)](https://packagist.org/packages/dereuromark/cakephp-calendar) 9 | [![Coding Standards](https://img.shields.io/badge/cs-PSR--2--R-yellow.svg)](https://github.com/php-fig-rectified/fig-rectified-standards) 10 | 11 | A plugin to render simple calendars. 12 | 13 | This branch is for **CakePHP 5.1+**. For details see [version map](https://github.com/dereuromark/cakephp-calendar/wiki#cakephp-version-map). 14 | 15 | ## Features 16 | - Simple and robust 17 | - No JS needed, more responsive than solutions like fullcalendar 18 | - Persistent `year/month` URL pieces (copy-paste and link/redirect friendly) 19 | - IcalView class for `.ics` calendar file output. 20 | 21 | ## Demo 22 | See the demo [Calendar example](https://sandbox.dereuromark.de/sandbox/calendar) at the sandbox. 23 | 24 | ## Setup 25 | ``` 26 | composer require dereuromark/cakephp-calendar 27 | ``` 28 | 29 | Then make sure the plugin is loaded in bootstrap: 30 | ``` 31 | bin/cake plugin load Calendar 32 | ``` 33 | 34 | ## Usage 35 | See [Documentation](/docs). 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dereuromark/cakephp-calendar", 3 | "description": "A CakePHP plugin to easily create calendars.", 4 | "license": "MIT", 5 | "type": "cakephp-plugin", 6 | "keywords": [ 7 | "cakephp", 8 | "plugin", 9 | "calendar", 10 | "helper", 11 | "events" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Mark Scherer", 16 | "homepage": "https://www.dereuromark.de", 17 | "role": "Maintainer" 18 | } 19 | ], 20 | "homepage": "https://github.com/dereuromark/cakephp-calendar", 21 | "support": { 22 | "source": "https://github.com/dereuromark/cakephp-calendar" 23 | }, 24 | "require": { 25 | "php": ">=8.1", 26 | "cakephp/cakephp": "^5.1.1" 27 | }, 28 | "require-dev": { 29 | "fig-r/psr2r-sniffer": "dev-master", 30 | "phpunit/phpunit": "^10.5 || ^11.5 || ^12.1" 31 | }, 32 | "minimum-stability": "stable", 33 | "prefer-stable": true, 34 | "autoload": { 35 | "psr-4": { 36 | "Calendar\\": "src/", 37 | "Calendar\\Test\\Fixture\\": "tests/Fixture/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Calendar\\Test\\": "tests/", 43 | "TestApp\\": "tests/TestApp/src/" 44 | } 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "dealerdirect/phpcodesniffer-composer-installer": true 49 | } 50 | }, 51 | "scripts": { 52 | "cs-check": "phpcs --extensions=php", 53 | "cs-fix": "phpcbf --extensions=php", 54 | "lowest": "validate-prefer-lowest", 55 | "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json", 56 | "stan": "phpstan analyse", 57 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^2.0.0 && mv composer.backup composer.json", 58 | "test": "phpunit", 59 | "test-coverage": "phpunit --log-junit tmp/coverage/unitreport.xml --coverage-html tmp/coverage --coverage-clover tmp/coverage/coverage.xml" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # CakePHP Calendar plugin 2 | 3 | ## Usage 4 | Make sure your calendar items table (e.g. EventsTable) has added the Calendar behavior in `initialize()` method: 5 | ```php 6 | // If needed, also provide your config 7 | $this->addBehavior('Calendar.Calendar', [ 8 | 'field' => 'beginning', 9 | 'endField' => 'end', 10 | 'scope' => ['invisible' => false], 11 | ]); 12 | ``` 13 | Now the `find('calendar')` custom finder is available on this table class. 14 | 15 | Load the component in your controller: 16 | ```php 17 | $this->loadComponent('Calendar.Calendar'); 18 | ``` 19 | 20 | And also your helper in the View class: 21 | ```php 22 | $this->loadHelper('Calendar.Calendar'); 23 | ``` 24 | 25 | Your action: 26 | ```php 27 | /** 28 | * @param string|null $year 29 | * @param string|null $month 30 | * @return void 31 | */ 32 | public function calendar($year = null, $month = null) { 33 | $this->Calendar->init($year, $month); 34 | 35 | // Fetch calendar items (like events, birthdays, ...) 36 | $options = [ 37 | 'year' => $this->Calendar->year(), 38 | 'month' => $this->Calendar->month(), 39 | ]; 40 | $events = $this->Events->find('calendar', ...$options); 41 | 42 | $this->set(compact('events')); 43 | } 44 | ``` 45 | 46 | In your index template: 47 | ```php 48 | Html->link($event->title, ['action' => 'view', $event->id]); 51 | $this->Calendar->addRow($event->date, $content, ['class' => 'event']); 52 | } 53 | 54 | echo $this->Calendar->render(); 55 | ?> 56 | 57 | Calendar->isCurrentMonth()) { ?> 58 | Html->link(__('Jump to the current month') . '...', ['action' => 'index'])?> 59 | 60 | ``` 61 | 62 | And in your view template you can have a backlink as easy as: 63 | ```php 64 | Html->link( 65 | __('List {0}', __('Events')), 66 | $this->Calendar->calendarUrlArray(['action' => 'index'], $event->date) 67 | ); ?> 68 | ``` 69 | 70 | It will redirect back to the current year and month this calendar item has been linked from. 71 | So you have a persistent calendar - even with some clicking around, the user will still be able to navigate very easily through the calendar items. 72 | 73 | #### Multi-day events 74 | In case you have a beginning and end for dates, and those can span over multiple days, use: 75 | ```php 76 | Calendar->addRowFromTo($event->beginning, $event->end, $content, $attr); 81 | } 82 | 83 | echo $this->Calendar->render(); 84 | ?> 85 | ``` 86 | 87 | ### Configuration 88 | 89 | #### Integrity 90 | The component validates both year and month input and throws 404 exception for invalid ones. 91 | 92 | The component has a max limit in each direction, defined by init() call: 93 | ```php 94 | $this->Calendar->init($year, $month, 5); 95 | ``` 96 | This will allow the calendar to work 5 years in both directions. Out of bounds are 404 exceptions. 97 | The helper knows not to generate links for over the limit dates. 98 | 99 | #### Presentation 100 | You can configure the URL elements to contain the month either as number (default) or text. 101 | ``` 102 | /controller/action/2017/08 103 | /controller/action/2017/august 104 | ``` 105 | When loading the helper, pass `'monthAsString' => true` for the textual option. 106 | 107 | 108 | ## ICalendar files (ical/ics) 109 | You can easily render out machine readable calendar events using the [Ical](https://en.wikipedia.org/wiki/ICalendar) format. 110 | 111 | In your routes.php file you need to enable `ics` as extension: 112 | ```php 113 | Router::extensions([..., 'ics']); 114 | ``` 115 | 116 | Then inside your controller just set a custom view class for this extension: 117 | ```php 118 | use Calendar\View\IcalView; 119 | 120 | /** 121 | * @return array 122 | */ 123 | public function viewClasses(): array { 124 | if (!$this->request->getParam('_ext')) { 125 | return []; 126 | } 127 | 128 | return [IcalView::class]; 129 | } 130 | ``` 131 | 132 | Let's say we want to render `/events/view/1.ics` now. 133 | Now it will look inside a subfolder for a PHP file here: `Template/Events/ics/view.php`. 134 | Inside this template just use any Ical library of your choice to output this event: 135 | 136 | ```php 137 | [ 145 | 'SUMMARY' => $event->name, 146 | 'DTSTART' => $event->beginning, 147 | 'DTEND' => $event->end, 148 | 'DESCRIPTION' => $event->description, 149 | 'GEO' => $event->lat . ';' . $event->lng, 150 | 'URL' => $event->url, 151 | ], 152 | ]); 153 | echo $vcalendar->serialize(); 154 | ``` 155 | This uses the [sabre-io/vobject](https://github.com/sabre-io/vobject) library (that you need to composer install then). 156 | 157 | You could also make your own helper and use that instead: 158 | ```php 159 | $calendarEvent = $this->Ical->newEvent(); 160 | $calendarEvent->set...(); 161 | $this->Ical->addEvent($calendarEvent); 162 | 163 | echo $this->Ical->render(); 164 | ``` 165 | 166 | I didn't want to hard-link this plugin to a specific renderer. This way you keep complete flexibility here while being able to use the view class as convenience wrapper. 167 | 168 | For a larger list of events, you can also look into e.g. 169 | - https://github.com/spatie/icalendar-generator 170 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/ 7 | tests/ 8 | 9 | /TestApp/templates/ 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src/ 5 | bootstrapFiles: 6 | - tests/bootstrap.php 7 | ignoreErrors: 8 | - identifier: missingType.iterableValue 9 | - identifier: missingType.generics 10 | 11 | -------------------------------------------------------------------------------- /src/CalendarPlugin.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public array $monthList = [ 24 | 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december']; 25 | 26 | /** 27 | * @var array 28 | */ 29 | public array $dayList = [ 30 | 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 31 | ]; 32 | 33 | public ?int $year = null; 34 | 35 | public ?int $month = null; 36 | 37 | public ?int $day = null; 38 | 39 | /** 40 | * @param string|int $year Year 41 | * @param string|int $month Month 42 | * @param int $span Years in both directions 43 | * @param array $options To be passed to previous and next methods 44 | * @throws \Cake\Http\Exception\NotFoundException 45 | * @return void 46 | */ 47 | public function init($year, $month, int $span = 10, array $options = []): void { 48 | if (!is_numeric($month)) { 49 | $month = $this->retrieveMonth($month); 50 | } 51 | $year = (int)$year; 52 | $month = (int)$month; 53 | 54 | if (!$year && !$month) { 55 | $year = (int)date('Y'); 56 | $month = (int)date('n'); 57 | } 58 | 59 | $current = (int)date('Y'); 60 | 61 | if (!$month || $year < $current - $span || $year > $current + $span) { 62 | throw new NotFoundException('Invalid date'); 63 | } 64 | 65 | $this->year = $year; 66 | $this->month = $month; 67 | 68 | if ($month < 1 || $month > 12) { 69 | throw new NotFoundException('Invalid date'); 70 | } 71 | 72 | $viewVars = $options + [ 73 | 'year' => $this->year, 74 | 'month' => $this->month, 75 | 'span' => $span, 76 | ]; 77 | 78 | $this->getController()->set('_calendar', $viewVars); 79 | } 80 | 81 | /** 82 | * @return int 83 | */ 84 | public function year() { 85 | if (!$this->year) { 86 | throw new RuntimeException('Make sure to first call init().'); 87 | } 88 | 89 | return $this->year; 90 | } 91 | 92 | /** 93 | * @return int 94 | */ 95 | public function month() { 96 | if (!$this->month) { 97 | throw new RuntimeException('Make sure to first call init().'); 98 | } 99 | 100 | return $this->month; 101 | } 102 | 103 | /** 104 | * @return string 105 | */ 106 | public function monthAsString() { 107 | if (!$this->month) { 108 | throw new RuntimeException('Make sure to first call init().'); 109 | } 110 | 111 | return $this->asString($this->month); 112 | } 113 | 114 | /** 115 | * Month as integer value 1..12 or 0 on error 116 | * february => 2 117 | * 118 | * @param string $month 119 | * @return int 120 | */ 121 | public function retrieveMonth($month) { 122 | if (!$month) { 123 | return 0; 124 | } 125 | $month = mb_strtolower($month); 126 | if (in_array($month, $this->monthList, true)) { 127 | $keys = array_keys($this->monthList, $month); 128 | 129 | return $keys[0] + 1; 130 | } 131 | 132 | return 0; 133 | } 134 | 135 | /** 136 | * Day as integer value 1..31 or 0 on error 137 | * february => 2 138 | * 139 | * @param string $day 140 | * @param int|null $month 141 | * @return int 142 | */ 143 | public function retrieveDay($day, $month = null) { 144 | $day = (int)$day; 145 | if ($day < 1 || $day > 31) { 146 | return 0; 147 | } 148 | 149 | // TODO check on month days! 150 | 151 | return $day; 152 | } 153 | 154 | /** 155 | * @return array 156 | */ 157 | public function months() { 158 | return $this->monthList; 159 | } 160 | 161 | /** 162 | * @return array 163 | */ 164 | public function days() { 165 | return $this->dayList; 166 | } 167 | 168 | /** 169 | * Converts integer to x-digit string 170 | * 1 => 01, 12 => 12 171 | * 172 | * @param int $number 173 | * @param int $digits 174 | * @return string 175 | */ 176 | protected function asString($number, $digits = 2) { 177 | $number = (string)$number; 178 | $count = mb_strlen($number); 179 | while ($count < $digits) { 180 | $number = '0' . $number; 181 | $count++; 182 | } 183 | 184 | return $number; 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/Model/Behavior/CalendarBehavior.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected array $_defaultConfig = [ 37 | 'field' => 'date', 38 | 'endField' => null, 39 | 'implementedFinders' => [ 40 | 'calendar' => 'findCalendar', 41 | ], 42 | 'scope' => [], 43 | ]; 44 | 45 | /** 46 | * Constructor 47 | * 48 | * Merges config with the default and store in the config property 49 | * 50 | * Does not retain a reference to the Table object. If you need this 51 | * you should override the constructor. 52 | * 53 | * @param \Cake\ORM\Table $table The table this behavior is attached to. 54 | * @param array $config The config for this behavior. 55 | */ 56 | public function __construct(Table $table, array $config = []) { 57 | $defaults = (array)Configure::read('Calendar'); 58 | parent::__construct($table, $config + $defaults); 59 | 60 | $this->_table = $table; 61 | } 62 | 63 | /** 64 | * Custom finder for Calendars field. 65 | * 66 | * Options: 67 | * - year (required), best to use CalendarBehavior::YEAR constant 68 | * - month (required), best to use CalendarBehavior::MONTH constant 69 | * 70 | * @param \Cake\ORM\Query\SelectQuery $query Query. 71 | * @param array $options Array of options as described above 72 | * @return \Cake\ORM\Query\SelectQuery 73 | */ 74 | public function findCalendar(SelectQuery $query, array $options): SelectQuery { 75 | $field = $this->getConfig('field'); 76 | 77 | $year = $options[static::YEAR]; 78 | $month = $options[static::MONTH]; 79 | 80 | $from = new DateTime($year . '-' . $month . '-01'); 81 | $lastDayOfMonth = $from->daysInMonth; 82 | 83 | $to = new DateTime($year . '-' . $month . '-' . $lastDayOfMonth . ' 23:59:59'); 84 | 85 | $conditions = [ 86 | $field . ' >=' => $from, 87 | $field . ' <=' => $to, 88 | ]; 89 | if ($this->getConfig('endField')) { 90 | $endField = $this->getConfig('endField'); 91 | 92 | $conditions = [ 93 | 'OR' => [ 94 | [ 95 | $field . ' <=' => $to, 96 | $endField . ' >' => $from, 97 | ], 98 | $conditions, 99 | ], 100 | ]; 101 | } 102 | 103 | $query->where($conditions); 104 | if ($this->getConfig('scope')) { 105 | $query->andWhere($this->getConfig('scope')); 106 | } 107 | 108 | return $query; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/View/Helper/CalendarHelper.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | protected array $weekendDayIndexes = []; 39 | 40 | /** 41 | * @var array 42 | */ 43 | protected array $dayList = []; 44 | 45 | /** 46 | * @var array 47 | */ 48 | protected array $localizedDayList = []; 49 | 50 | /** 51 | * @var array 52 | */ 53 | protected array $_defaultConfig = [ 54 | 'monthAsString' => false, 55 | 'multiLabelSuffix' => ' (Day {0})', 56 | 'timezone' => null, 57 | ]; 58 | 59 | /** 60 | * Containing all rows 61 | */ 62 | public array $dataContainer = []; 63 | 64 | /** 65 | * @param array $config 66 | * @return void 67 | */ 68 | public function initialize(array $config): void { 69 | $this->dataContainer = []; 70 | $intlCalendar = IntlCalendar::createInstance(); 71 | 72 | $firstDayLabel = 'Monday'; 73 | $firstDayOfWeek = $intlCalendar->getFirstDayOfWeek(); 74 | switch ($firstDayOfWeek) { 75 | case IntlCalendar::DOW_SUNDAY: 76 | $firstDayLabel = 'Sunday'; 77 | 78 | break; 79 | case IntlCalendar::DOW_MONDAY: 80 | $firstDayLabel = 'Monday'; 81 | 82 | break; 83 | case IntlCalendar::DOW_SATURDAY: 84 | $firstDayLabel = 'Saturday'; 85 | 86 | break; 87 | } 88 | 89 | $this->dayList = $this->localizedDayList = []; 90 | $firstDayOfWeek = new DateTime($firstDayLabel); 91 | foreach (range(0, 6) as $modifier) { 92 | $this->dayList[] = strtolower( 93 | (string)$firstDayOfWeek 94 | ->addDays($modifier) 95 | ->i18nFormat('ccc', null, 'en-GB'), 96 | ); 97 | $this->localizedDayList[] = (string)$firstDayOfWeek 98 | ->addDays($modifier) 99 | ->i18nFormat('ccc'); 100 | $intlCalendarDayOfWeek = (int)$firstDayOfWeek 101 | ->addDays($modifier) 102 | ->i18nFormat('c', null, 'en-US'); 103 | 104 | if ($intlCalendar->getDayOfWeekType($intlCalendarDayOfWeek) == IntlCalendar::DOW_TYPE_WEEKEND) { 105 | $this->weekendDayIndexes[] = $modifier; 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * @param \Cake\Chronos\Chronos $date 112 | * @param string $content 113 | * @param array $options 114 | * @return void 115 | */ 116 | public function addRow(Chronos $date, string $content, array $options = []): void { 117 | if (!$content) { 118 | return; 119 | } 120 | $day = $this->retrieveDayFromDate($date); 121 | $this->dataContainer[$day][] = $this->Html->tag('li', $content, $options); 122 | } 123 | 124 | /** 125 | * @param \Cake\Chronos\Chronos $from 126 | * @param \Cake\Chronos\Chronos $to 127 | * @param string $content 128 | * @param array $options 129 | * 130 | * @return void 131 | */ 132 | public function addRowFromTo(Chronos $from, Chronos $to, string $content, array $options = []): void { 133 | if (!$content) { 134 | return; 135 | } 136 | 137 | $from = clone $from; 138 | $from = $from->setTime(0, 0, 0); 139 | $month = $this->_View->get('_calendar')['month']; 140 | 141 | $days = [ 142 | ]; 143 | $count = 0; 144 | while ($from <= $to) { 145 | if ($from->month === $month) { 146 | $days[$count] = $this->retrieveDayFromDate($from); 147 | } 148 | $from = $from->addDays(1); 149 | $count++; 150 | } 151 | 152 | $suffix = ''; 153 | if ($count > 1) { 154 | $suffix = $this->getConfig('multiLabelSuffix'); 155 | } 156 | foreach ($days as $i => $day) { 157 | $suffixTranslated = __($suffix, $i + 1); 158 | $this->dataContainer[$day][] = $this->Html->tag('li', $content . $suffixTranslated, $options); 159 | } 160 | } 161 | 162 | /** 163 | * Generates a calendar for the specified by the month and year params and populates 164 | * it with the content of the data container array 165 | * 166 | * @return string HTML code to display calendar in view 167 | */ 168 | public function render(): string { 169 | $str = ''; 170 | 171 | $day = 1; 172 | $today = 0; 173 | 174 | if (empty($this->_View->get('_calendar'))) { 175 | throw new RuntimeException('You need to load Calendar.Calendar component for this helper to work.'); 176 | } 177 | 178 | $year = $this->_View->get('_calendar')['year']; 179 | $month = $this->_View->get('_calendar')['month']; 180 | 181 | $data = $this->dataContainer; 182 | $now = new DateTime(null, $this->getConfig('timezone')); 183 | 184 | $currentYear = (int)$now->format('Y'); 185 | $currentMonth = (int)$now->format('n'); 186 | if ($year === $currentYear && $month === $currentMonth) { 187 | $today = (int)$now->format('j'); 188 | } 189 | 190 | $daysInMonth = date('t', (int)mktime(0, 0, 0, $month, 1, $year)); 191 | 192 | $firstDayInMonth = date('D', (int)mktime(0, 0, 0, $month, 1, $year)); 193 | $firstDayInMonth = strtolower($firstDayInMonth); 194 | 195 | $monthObject = DateTime::createFromFormat( 196 | 'Y-m-d', 197 | $year . '-' . $month . '-15', // 15th day of selected month, to avoid timezone screwyness 198 | ); 199 | 200 | $str .= ''; 201 | 202 | $str .= ''; 203 | 204 | $str .= ''; 213 | 214 | $str .= ''; 215 | 216 | for ($i = 0; $i < 7; $i++) { 217 | $str .= ''; 218 | } 219 | 220 | $str .= ''; 221 | 222 | $str .= ''; 223 | 224 | $str .= ''; 225 | 226 | while ($day <= $daysInMonth) { 227 | $str .= ''; 228 | 229 | for ($i = 0; $i < 7; $i++) { 230 | $cell = ' '; 231 | 232 | if (isset($data[$day])) { 233 | $cell = '
    ' . implode(PHP_EOL, $data[$day]) . '
'; 234 | } 235 | 236 | $class = ''; 237 | 238 | if (in_array($i, $this->weekendDayIndexes)) { 239 | $class = ' class="cell-weekend"'; 240 | } 241 | if ($day === $today && ($firstDayInMonth == $this->dayList[$i] || $day > 1) && ($day <= $daysInMonth)) { 242 | $class = ' class="cell-today"'; 243 | } 244 | 245 | if (($firstDayInMonth == $this->dayList[$i] || $day > 1) && ($day <= $daysInMonth)) { 246 | $str .= '
' . $day . '
' . $cell . '
'; 247 | $day++; 248 | } else { 249 | $str .= '
'; 250 | } 251 | } 252 | $str .= ''; 253 | } 254 | 255 | $str .= ''; 256 | 257 | $str .= '
'; 205 | 206 | $str .= $this->previousLink(); 207 | 208 | $str .= '' . $monthObject->i18nFormat('LLLL Y') . ''; 209 | 210 | $str .= $this->nextLink(); 211 | 212 | $str .= '
' . $this->localizedDayList[$i] . '
 
'; 258 | 259 | return $str; 260 | } 261 | 262 | /** 263 | * @param \Cake\Chronos\Chronos $date 264 | * @return int 265 | */ 266 | public function retrieveDayFromDate(Chronos $date): int { 267 | return (int)$date->format('d'); 268 | } 269 | 270 | /** 271 | * @param \Cake\Chronos\Chronos $date 272 | * @return int 273 | */ 274 | public function retrieveMonthFromDate(Chronos $date): int { 275 | return (int)$date->format('n'); 276 | } 277 | 278 | /** 279 | * @param \Cake\Chronos\Chronos $date 280 | * @return int 281 | */ 282 | public function retrieveYearFromDate(Chronos $date): int { 283 | return (int)$date->format('Y'); 284 | } 285 | 286 | /** 287 | * Generates a link back to the calendar from any view page. 288 | * 289 | * Specify action and if necessary controller, plugin, and prefix. 290 | * 291 | * @param array $url 292 | * @param \Cake\Chronos\Chronos $dateTime 293 | * @return array 294 | */ 295 | public function calendarUrlArray(array $url, Chronos $dateTime): array { 296 | $year = $this->retrieveYearFromDate($dateTime); 297 | $month = $this->retrieveMonthFromDate($dateTime); 298 | 299 | $currentYear = (int)date('Y'); 300 | $currentMonth = (int)date('n'); 301 | 302 | if ($year === $currentYear && $month === $currentMonth) { 303 | return $url; 304 | } 305 | 306 | $url[] = $year; 307 | $url[] = $this->formatMonth($month); 308 | 309 | return $url; 310 | } 311 | 312 | /** 313 | * @return string 314 | */ 315 | public function previousLink(): string { 316 | $year = $this->_View->get('_calendar')['year']; 317 | $month = $this->_View->get('_calendar')['month']; 318 | 319 | $currentYear = (int)date('Y'); 320 | $currentMonth = (int)date('n'); 321 | 322 | $flag = 0; 323 | if (!$year || !$month) { // just use current year & month 324 | $year = $currentYear; 325 | $month = $currentMonth; 326 | } 327 | if ($month > 0 && $month < 13) { 328 | $flag = 1; 329 | } 330 | if ($flag === 0) { 331 | $year = $currentYear; 332 | $month = $currentMonth; 333 | } 334 | 335 | $prevYear = $year; 336 | $prevMonth = (int)($month - 1); 337 | 338 | if ($prevMonth === 0) { 339 | $prevMonth = 12; 340 | $prevYear = $year - 1; 341 | } 342 | 343 | $span = $this->_View->get('_calendar')['span']; 344 | if ($prevYear < $currentYear - $span) { 345 | return ''; 346 | } 347 | 348 | if ($prevYear === $currentYear && $prevMonth === $currentMonth) { 349 | $prevMonth = $prevYear = null; 350 | } 351 | 352 | $url = [ 353 | $prevYear, 354 | $this->formatMonth($prevMonth), 355 | ]; 356 | 357 | $viewVars = $this->_View->get('_calendar'); 358 | if (!empty($viewVars['url'])) { 359 | $url = array_merge($url, $viewVars['url']); 360 | } 361 | 362 | return $this->Html->link(__('previous'), $url); 363 | } 364 | 365 | /** 366 | * @return string 367 | */ 368 | public function nextLink(): string { 369 | $year = $this->_View->get('_calendar')['year']; 370 | $month = $this->_View->get('_calendar')['month']; 371 | 372 | $currentYear = (int)date('Y'); 373 | $currentMonth = (int)date('n'); 374 | 375 | $flag = 0; 376 | if (!$year || !$month) { // just use current year & month 377 | $year = $currentYear; 378 | $month = $currentMonth; 379 | } 380 | if ($month > 0 && $month < 13 && (int)$year != 0) { 381 | $flag = 1; 382 | } 383 | if ($flag === 0) { 384 | $year = $currentYear; 385 | $month = $currentMonth; 386 | } 387 | 388 | $nextYear = $year; 389 | $nextMonth = (int)($month + 1); 390 | 391 | if ($nextMonth === 13) { 392 | $nextMonth = 1; 393 | $nextYear = $year + 1; 394 | } 395 | 396 | $span = $this->_View->get('_calendar')['span']; 397 | if ($nextYear > $currentYear + $span) { 398 | return ''; 399 | } 400 | 401 | if ($nextYear === $currentYear && $nextMonth === $currentMonth) { 402 | $nextMonth = $nextYear = null; 403 | } 404 | 405 | $url = [ 406 | $nextYear, 407 | $this->formatMonth($nextMonth), 408 | ]; 409 | 410 | $viewVars = $this->_View->get('_calendar'); 411 | if (!empty($viewVars['url'])) { 412 | $url = array_merge($url, $viewVars['url']); 413 | } 414 | 415 | return $this->Html->link(__('next'), $url); 416 | } 417 | 418 | /** 419 | * @return bool 420 | */ 421 | public function isCurrentMonth(): bool { 422 | $year = $this->_View->get('_calendar')['year']; 423 | $month = $this->_View->get('_calendar')['month']; 424 | 425 | return $year === (int)date('Y') && $month === (int)date('n'); 426 | } 427 | 428 | /** 429 | * @param int|null $month 430 | * @return string|null 431 | */ 432 | public function formatMonth(?int $month): ?string { 433 | if (!$month) { 434 | return null; 435 | } 436 | 437 | if ($this->getConfig('monthAsString')) { 438 | return $this->monthName($month); 439 | } 440 | 441 | return str_pad((string)$month, 2, '0', STR_PAD_LEFT); 442 | } 443 | 444 | /** 445 | * @param int $month 446 | * @return string|null 447 | */ 448 | public function monthName(int $month): ?string { 449 | if (!isset($this->monthList[$month - 1])) { 450 | return null; 451 | } 452 | 453 | return __(ucfirst($this->monthList[$month - 1])); 454 | } 455 | 456 | } 457 | -------------------------------------------------------------------------------- /src/View/Helper/GoogleCalendarHelper.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected array $_defaultConfig = [ 25 | 'url' => 'https://calendar.google.com/calendar/render', 26 | ]; 27 | 28 | /** 29 | * Generates a calendar URL for google. 30 | * 31 | * @see https://github.com/InteractionDesignFoundation/add-event-to-calendar-docs/blob/main/services/google.md 32 | * 33 | * @param string $title 34 | * @param array $dateFromTo 35 | * @param array $details 36 | * 37 | * @return string HTML code to display calendar in view 38 | */ 39 | public function url(string $title, array $dateFromTo, array $details = []): string { 40 | $url = $this->getConfig('url'); 41 | $url .= '?action=TEMPLATE'; 42 | 43 | $query = []; 44 | $query[] = 'text=' . urlencode($title); 45 | 46 | $dates = []; 47 | if (!empty($dateFromTo['from'])) { 48 | /** @var \Cake\I18n\DateTime|\Cake\I18n\Date|string $from */ 49 | $from = $dateFromTo['from']; 50 | if ($from instanceof Date) { 51 | $from = $from->year . $from->month . $from->day; 52 | } elseif (!is_string($from)) { 53 | $from = $from->toIso8601String(); 54 | $from = str_replace('-', '', $from); 55 | } 56 | $dates[] = $from; 57 | } 58 | if (!empty($dateFromTo['to'])) { 59 | /** @var \Cake\I18n\DateTime|\Cake\I18n\Date|string $to */ 60 | $to = $dateFromTo['to']; 61 | if ($to instanceof Date) { 62 | $to = $to->year . $to->month . $to->day; 63 | } elseif (!is_string($to)) { 64 | $to = $to->toIso8601String(); 65 | $to = str_replace('-', '', $to); 66 | } 67 | $dates[] = $to; 68 | } elseif (!empty($dateFromTo['from']) && $dateFromTo['from'] instanceof Date) { 69 | $to = $dateFromTo['from']->addDays(1); 70 | $dates[] = $to->year . $to->month . $to->day; 71 | 72 | } 73 | 74 | if (!$dates) { 75 | throw new InvalidArgumentException('Missing required input for date (from)'); 76 | } 77 | $query[] = 'dates=' . urlencode(implode('/', $dates)); 78 | 79 | if (!empty($details['details'])) { 80 | $query[] = 'details=' . urlencode($details['details']); 81 | } 82 | if (!empty($details['location'])) { 83 | $query[] = 'location=' . urlencode($details['location']); 84 | } 85 | if (!empty($details['ctz'])) { 86 | $query[] = 'ctz=' . urlencode($details['ctz']); 87 | } 88 | 89 | //TODO: sprop etc 90 | 91 | $url .= '&' . implode('&', $query); 92 | 93 | return $url; 94 | } 95 | 96 | /** 97 | * Generates a calendar link for google. 98 | * 99 | * @param string $title 100 | * @param array $dateFromTo 101 | * @param array $details 102 | * 103 | * @return string HTML code to display calendar in view 104 | */ 105 | public function link(string $title, array $dateFromTo, array $details = []): string { 106 | return $this->Html->link($title, $this->url($title, $dateFromTo, $details)); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/View/IcalView.php: -------------------------------------------------------------------------------- 1 | withType('ics'); 42 | } 43 | 44 | parent::__construct($request, $response, $eventManager, $viewOptions); 45 | 46 | $this->disableAutoLayout(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/Fixture/EventsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => 10, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], 20 | 'title' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 21 | 'description' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 22 | 'beginning' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 23 | 'end' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 24 | '_constraints' => [ 25 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 26 | ], 27 | ]; 28 | 29 | /** 30 | * Records 31 | * 32 | * @var array 33 | */ 34 | public array $records = [ 35 | [ 36 | 'id' => 1, 37 | 'title' => 'Lorem ipsum dolor sit amet', 38 | 'description' => 'Lorem ipsum dolor sit amet, aliquet feugiat. Convallis morbi fringilla gravida, phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla vestibulum massa neque ut et, id hendrerit sit, feugiat in taciti enim proin nibh, tempor dignissim, rhoncus duis vestibulum nunc mattis convallis.', 39 | 'beginning' => '2015-08-17 15:32:36', 40 | 'end' => '2015-08-17 15:32:36', 41 | ], 42 | ]; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Component/CalendarComponentTest.php: -------------------------------------------------------------------------------- 1 | Controller = new CalendarComponentTestController(new ServerRequest()); 21 | $this->Controller->startupProcess(); 22 | } 23 | 24 | /** 25 | * @return void 26 | */ 27 | public function tearDown(): void { 28 | parent::tearDown(); 29 | 30 | unset($this->Controller->Calendar); 31 | unset($this->Controller); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | public function testInit() { 38 | $this->Controller->Calendar->init('2016', '02'); 39 | 40 | $this->assertSame(2016, $this->Controller->Calendar->year()); 41 | $this->assertSame(2, $this->Controller->Calendar->month()); 42 | } 43 | 44 | /** 45 | * @return void 46 | */ 47 | public function testInitFromString() { 48 | $this->Controller->Calendar->init('2016', 'february'); 49 | 50 | $this->assertSame(2016, $this->Controller->Calendar->year()); 51 | $this->assertSame(2, $this->Controller->Calendar->month()); 52 | } 53 | 54 | /** 55 | * @return void 56 | */ 57 | public function testInitInvalid() { 58 | $this->expectException(NotFoundException::class); 59 | 60 | $this->Controller->Calendar->init('2016', ''); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/CalendarBehaviorTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected array $fixtures = [ 16 | 'plugin.Calendar.Events', 17 | ]; 18 | 19 | /** 20 | * @var \Cake\ORM\Table; 21 | */ 22 | protected Table $Events; 23 | 24 | /** 25 | * @var array 26 | */ 27 | protected array $config = [ 28 | 'field' => 'beginning', 29 | ]; 30 | 31 | /** 32 | * setUp 33 | * 34 | * @return void 35 | */ 36 | public function setUp(): void { 37 | parent::setUp(); 38 | 39 | $this->Events = TableRegistry::getTableLocator()->get('Calendar.Events'); 40 | $this->Events->addBehavior('Calendar.Calendar', $this->config); 41 | 42 | $this->truncate(); 43 | $this->_addFixtureData(); 44 | } 45 | 46 | /** 47 | * @return void 48 | */ 49 | protected function truncate() { 50 | /** @var \Cake\Database\Schema\SqlGeneratorInterface $schema */ 51 | $schema = $this->Events->getSchema(); 52 | $sql = $schema->truncateSql($this->Events->getConnection()); 53 | foreach ($sql as $snippet) { 54 | $this->Events->getConnection()->execute($snippet); 55 | } 56 | } 57 | 58 | /** 59 | * @return void 60 | */ 61 | public function tearDown(): void { 62 | parent::tearDown(); 63 | 64 | unset($this->Events); 65 | TableRegistry::getTableLocator()->clear(); 66 | } 67 | 68 | /** 69 | * @return void 70 | */ 71 | public function testFind() { 72 | $options = [ 73 | 'month' => 12, 74 | 'year' => (int)date('Y'), 75 | ]; 76 | 77 | $events = $this->Events->find('calendar', ...$options); 78 | 79 | $eventList = array_values($events->find('list')->toArray()); 80 | $expected = [ 81 | 'One', 82 | '4 days', 83 | 'Over new years eve', 84 | ]; 85 | $this->assertEquals($expected, $eventList); 86 | } 87 | 88 | /** 89 | * Gets a new Entity 90 | * 91 | * @param array $data 92 | * @return \Cake\ORM\Entity 93 | */ 94 | protected function _getEntity($data) { 95 | return new Entity($data); 96 | } 97 | 98 | /** 99 | * @throws \Cake\Http\Exception\InternalErrorException 100 | * @return void 101 | */ 102 | protected function _addFixtureData() { 103 | $data = [ 104 | [ 105 | 'title' => 'Wrong', 106 | 'beginning' => date('Y') . '-11-30', 107 | ], 108 | [ 109 | 'title' => 'One', 110 | 'beginning' => date('Y') . '-12-28', 111 | ], 112 | [ 113 | 'title' => '4 days', 114 | 'beginning' => date('Y') . '-12-14', 115 | 'end' => date('Y') . '-12-18', 116 | ], 117 | [ 118 | 'title' => 'Over new years eve', 119 | 'beginning' => date('Y') . '-12-29', 120 | 'end' => ((int)date('Y') + 1) . '-01-02', 121 | ], 122 | ]; 123 | 124 | foreach ($data as $row) { 125 | $entity = $this->_getEntity($row); 126 | $this->Events->saveOrFail($entity); 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/CalendarHelperTest.php: -------------------------------------------------------------------------------- 1 | '/events'])) 29 | ->withParam('controller', 'Events') 30 | ->withParam('action', 'index'); 31 | 32 | $this->View = new View($request); 33 | $this->Calendar = new CalendarHelper($this->View); 34 | $this->Calendar->getView()->setRequest($request); 35 | 36 | Router::reload(); 37 | Router::defaultRouteClass(DashedRoute::class); 38 | $builder = Router::createRouteBuilder('/'); 39 | $builder->fallbacks(DashedRoute::class); 40 | $builder->connect('/{controller}/{action}/*'); 41 | Router::setRequest($this->Calendar->getView()->getRequest()); 42 | } 43 | 44 | /** 45 | * tearDown method 46 | * 47 | * @return void 48 | */ 49 | public function tearDown(): void { 50 | parent::tearDown(); 51 | 52 | unset($this->Calendar); 53 | } 54 | 55 | /** 56 | * @return void 57 | */ 58 | public function testRenderEmpty() { 59 | $this->View->set('_calendar', [ 60 | 'span' => 3, 61 | 'year' => 2010, 62 | 'month' => 12, 63 | ]); 64 | 65 | $result = $this->Calendar->render(); 66 | $this->assertStringContainsString('', $result); 67 | } 68 | 69 | /** 70 | * @return void 71 | */ 72 | public function testRenderEnUs() { 73 | I18n::setLocale('en-us'); // English - United States 74 | $this->Calendar = new CalendarHelper($this->View); 75 | 76 | $this->View->set('_calendar', [ 77 | 'span' => 3, 78 | 'year' => 2010, 79 | 'month' => 12, 80 | ]); 81 | 82 | $result = $this->Calendar->render(); 83 | $this->assertStringContainsString('', $result); 84 | 85 | // Sunday is the first day of the week 86 | $this->assertStringContainsString('', $result); 87 | 88 | // Saturday and Sunday are "weekend" days 89 | $this->assertStringContainsString('', $result); 108 | 109 | // Monday is the first day of the week 110 | $this->assertStringContainsString('', $result); 111 | 112 | // Saturday and Sunday are "weekend" days 113 | $this->assertStringContainsString('', $result); 132 | 133 | // Saturday is the first day of the week 134 | $this->assertStringContainsString('', $result); 135 | 136 | // Friday and Saturday are "weekend" days 137 | $this->assertStringContainsString(''; 175 | $this->assertStringContainsString($expected, $result); 176 | 177 | $this->assertStringContainsString(''; 193 | $this->assertStringContainsString($expected, $result); 194 | 195 | $this->assertStringContainsString('
December 2010
Sun
4', $result); 90 | $this->assertStringContainsString('
5', $result); 91 | } 92 | 93 | /** 94 | * @return void 95 | */ 96 | public function testRenderDeDe() { 97 | I18n::setLocale('de-de'); // German - Germany 98 | $this->Calendar = new CalendarHelper($this->View); 99 | 100 | $this->View->set('_calendar', [ 101 | 'span' => 3, 102 | 'year' => 2010, 103 | 'month' => 12, 104 | ]); 105 | 106 | $result = $this->Calendar->render(); 107 | $this->assertStringContainsString('
Dezember 2010
Mo
4', $result); 114 | $this->assertStringContainsString('
5', $result); 115 | } 116 | 117 | /** 118 | * @return void 119 | */ 120 | public function testRenderArDz() { 121 | I18n::setLocale('ar-dz'); // Arabic - Algeria 122 | $this->Calendar = new CalendarHelper($this->View); 123 | 124 | $this->View->set('_calendar', [ 125 | 'span' => 3, 126 | 'year' => 2010, 127 | 'month' => 12, 128 | ]); 129 | 130 | $result = $this->Calendar->render(); 131 | $this->assertStringContainsString('
ديسمبر 2010
السبت
3', $result); 138 | $this->assertStringContainsString('
4', $result); 139 | } 140 | 141 | /** 142 | * @return void 143 | */ 144 | public function testRender() { 145 | $this->View->set('_calendar', [ 146 | 'span' => 3, 147 | 'year' => date('Y'), 148 | 'month' => 12, 149 | ]); 150 | 151 | $this->Calendar->addRow(new DateTime(date('Y') . '-12-02 11:12:13'), 'Foo Bar', ['class' => 'event']); 152 | 153 | $result = $this->Calendar->render(); 154 | 155 | $expected = '
2
  • Foo Bar
'; 156 | $this->assertStringContainsString($expected, $result); 157 | 158 | $this->assertStringContainsString('
assertStringContainsString('View->set('_calendar', [ 167 | 'span' => 3, 168 | 'year' => date('Y') - 4, 169 | 'month' => 12, 170 | ]); 171 | 172 | $result = $this->Calendar->render(); 173 | 174 | $expected = '>View->set('_calendar', [ 185 | 'span' => 3, 186 | 'year' => date('Y') + 4, 187 | 'month' => 12, 188 | ]); 189 | 190 | $result = $this->Calendar->render(); 191 | 192 | $expected = '>View = new View(); 26 | $this->GoogleCalendar = new GoogleCalendarHelper($this->View); 27 | } 28 | 29 | /** 30 | * tearDown method 31 | * 32 | * @return void 33 | */ 34 | public function tearDown(): void { 35 | parent::tearDown(); 36 | 37 | unset($this->GoogleCalendar); 38 | } 39 | 40 | /** 41 | * @return void 42 | */ 43 | public function testUrl() { 44 | $details = [ 45 | 'details' => 'My details', 46 | 'location' => 'My location', 47 | //'ctz' => 'Europe/Berlin', 48 | ]; 49 | $fromTo = [ 50 | 'from' => new DateTime('2023-12-02 15:00:00'), 51 | 'to' => new DateTime('2023-12-02 18:00:00'), 52 | ]; 53 | 54 | $result = $this->GoogleCalendar->url('My title', $fromTo, $details); 55 | $expected = 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=My+title&dates=20231202T15%3A00%3A00%2B00%3A00%2F20231202T18%3A00%3A00%2B00%3A00&details=My+details&location=My+location'; 56 | $this->assertSame($expected, $result); 57 | } 58 | 59 | /** 60 | * @return void 61 | */ 62 | public function testUrlDate() { 63 | $fromTo = [ 64 | 'from' => new Date('2023-12-02'), 65 | ]; 66 | 67 | $result = $this->GoogleCalendar->url('My title', $fromTo); 68 | $expected = 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=My+title&dates=2023122%2F2023123'; 69 | $this->assertSame($expected, $result); 70 | } 71 | 72 | /** 73 | * @return void 74 | */ 75 | public function testLinkDate() { 76 | $fromTo = [ 77 | 'from' => new Date('2023-12-02'), 78 | ]; 79 | 80 | $result = $this->GoogleCalendar->link('My title', $fromTo); 81 | $expected = 'My title'; 82 | $this->assertSame($expected, $result); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /tests/TestCase/View/IcalViewTest.php: -------------------------------------------------------------------------------- 1 | request = (new ServerRequest()) 29 | ->withParam('controller', 'Events') 30 | ->withParam('action', 'index') 31 | ->withParam('_ext', 'ics'); 32 | $this->response = new Response(); 33 | 34 | $this->icalView = new IcalView($this->request, $this->response); 35 | 36 | Router::defaultRouteClass(DashedRoute::class); 37 | $builder = Router::createRouteBuilder('/'); 38 | $builder->fallbacks(DashedRoute::class); 39 | $builder->connect('/{controller}/{action}/*'); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | public function tearDown(): void { 46 | parent::tearDown(); 47 | 48 | unset($this->icalView); 49 | } 50 | 51 | /** 52 | * @return void 53 | */ 54 | public function testRenderEmpty() { 55 | //$this->icalView->setviewVars[] = []; 56 | 57 | $result = $this->icalView->render('view'); 58 | $this->assertSame('', $result); 59 | 60 | $type = $this->icalView->getResponse()->getType(); 61 | $this->assertSame('text/calendar', $type); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'engine' => 'File', 39 | 'path' => CACHE, 40 | ], 41 | '_cake_translations_' => [ 42 | 'className' => 'File', 43 | 'prefix' => 'myapp_cake_translations_', 44 | 'path' => CACHE . 'persistent/', 45 | 'serialize' => true, 46 | 'duration' => '+10 seconds', 47 | ], 48 | '_cake_model_' => [ 49 | 'className' => 'File', 50 | 'prefix' => 'myapp_cake_model_', 51 | 'path' => CACHE . 'models/', 52 | 'serialize' => 'File', 53 | 'duration' => '+10 seconds', 54 | ], 55 | ]; 56 | 57 | Cache::setConfig($cache); 58 | 59 | if (file_exists(CONFIG . 'app_local.php')) { 60 | Configure::load('app_local', 'default'); 61 | } 62 | 63 | Configure::write('App', [ 64 | 'namespace' => 'App', 65 | 'encoding' => 'utf-8', 66 | 'paths' => [ 67 | 'templates' => dirname(__FILE__) . DS . 'TestApp' . DS . 'templates' . DS, 68 | ], 69 | ]); 70 | 71 | // Ensure default test connection is defined 72 | if (!getenv('DB_URL')) { 73 | putenv('DB_URL=sqlite:///:memory:'); 74 | } 75 | 76 | ConnectionManager::setConfig('test', [ 77 | 'url' => getenv('DB_URL') ?: null, 78 | 'timezone' => 'UTC', 79 | 'quoteIdentifiers' => true, 80 | 'cacheMetadata' => true, 81 | ]); 82 | 83 | if (env('FIXTURE_SCHEMA_METADATA')) { 84 | $loader = new SchemaLoader(); 85 | $loader->loadInternalFile(env('FIXTURE_SCHEMA_METADATA')); 86 | } 87 | -------------------------------------------------------------------------------- /tests/config/routes.php: -------------------------------------------------------------------------------- 1 | fallbacks(DashedRoute::class); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/schema.php: -------------------------------------------------------------------------------- 1 | $iterator 9 | */ 10 | $iterator = new DirectoryIterator(__DIR__ . DS . 'Fixture'); 11 | foreach ($iterator as $file) { 12 | if (!preg_match('/(\w+)Fixture.php$/', (string)$file, $matches)) { 13 | continue; 14 | } 15 | 16 | $name = $matches[1]; 17 | $tableName = null; 18 | $class = 'Calendar\\Test\\Fixture\\' . $name . 'Fixture'; 19 | try { 20 | $fieldsObject = (new ReflectionClass($class))->getProperty('fields'); 21 | $tableObject = (new ReflectionClass($class))->getProperty('table'); 22 | $tableName = $tableObject->getDefaultValue(); 23 | 24 | } catch (ReflectionException $e) { 25 | continue; 26 | } 27 | 28 | if (!$tableName) { 29 | $tableName = Inflector::underscore($name); 30 | } 31 | 32 | $array = $fieldsObject->getDefaultValue(); 33 | $constraints = $array['_constraints'] ?? []; 34 | $indexes = $array['_indexes'] ?? []; 35 | unset($array['_constraints'], $array['_indexes'], $array['_options']); 36 | $table = [ 37 | 'table' => $tableName, 38 | 'columns' => $array, 39 | 'constraints' => $constraints, 40 | 'indexes' => $indexes, 41 | ]; 42 | $tables[$tableName] = $table; 43 | } 44 | 45 | return $tables; 46 | --------------------------------------------------------------------------------