├── LICENSE ├── README.md ├── composer.json ├── config └── app.example.php ├── docs └── README.md ├── phpcs.xml ├── phpstan.neon ├── src ├── Controller │ ├── Admin │ │ └── QrCodeController.php │ └── QrCodeController.php ├── QrCodePlugin.php ├── Utility │ ├── Config.php │ ├── Formatter.php │ └── FormatterInterface.php └── View │ ├── Helper │ └── QrCodeHelper.php │ ├── QrCodePngView.php │ └── QrCodeView.php ├── templates ├── Admin │ └── QrCode │ │ ├── image.php │ │ └── index.php └── QrCode │ └── image.php └── tests └── TestCase ├── Controller ├── Admin │ └── QrCodeControllerTest.php └── QrCodeControllerTest.php ├── Utility └── FormatterTest.php └── View └── Helper └── QrCodeHelperTest.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP QrCode Plugin 2 | 3 | [![CI](https://github.com/dereuromark/cakephp-qrcode/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dereuromark/cakephp-qrcode/actions?query=workflow%3ACI+branch%3Amaster) 4 | [![Coverage Status](https://img.shields.io/codecov/c/github/dereuromark/cakephp-qrcode/master.svg)](https://app.codecov.io/github/dereuromark/cakephp-qrcode/tree/master) 5 | [![PHPStan](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg?style=flat)](https://phpstan.org/) 6 | [![Latest Stable Version](https://poser.pugx.org/dereuromark/cakephp-qrcode/v/stable.svg)](https://packagist.org/packages/dereuromark/cakephp-qrcode) 7 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/) 8 | [![License](https://poser.pugx.org/dereuromark/cakephp-qrcode/license.png)](https://packagist.org/packages/dereuromark/cakephp-qrcode) 9 | [![Total Downloads](https://poser.pugx.org/dereuromark/cakephp-qrcode/d/total.svg)](https://packagist.org/packages/dereuromark/cakephp-qrcode) 10 | 11 | QrCode plugin for CakePHP applications. 12 | 13 | This branch is for use with **CakePHP 5.1+**. For details see [version map](https://github.com/dereuromark/cakephp-qrcode/wiki#cakephp-version-map). 14 | 15 | ## Motivation 16 | 17 | Wraps [chillerlan/php-qrcode/](https://github.com/chillerlan/php-qrcode/) library to have an easy to use 18 | out-of-the-box interface for most common QR codes. 19 | 20 | ## Features 21 | 22 | Supports: 23 | - base64encoded (default) 24 | - svg/png as controller action generated on-the-fly image 25 | 26 | ## Install/Setup 27 | Installing the Plugin is pretty much as with every other CakePHP Plugin. 28 | 29 | Install using Packagist/Composer: 30 | ``` 31 | composer require dereuromark/cakephp-qrcode 32 | ``` 33 | 34 | The following command can enable the plugin: 35 | ``` 36 | bin/cake plugin load QrCode 37 | ``` 38 | or manually add it to your `Application` class. 39 | 40 | ### Usage 41 | See the **[Docs](docs/README.md)** for details. 42 | 43 | ## Demo 44 | See https://sandbox.dereuromark.de/sandbox/qr-code-examples 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dereuromark/cakephp-qrcode", 3 | "description": "CakePHP QR Code Plugin", 4 | "type": "cakephp-plugin", 5 | "keywords": [ 6 | "cakephp", 7 | "plugin", 8 | "qr code" 9 | ], 10 | "homepage": "https://github.com/dereuromark/cakephp-qrcode/", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Mark Scherer", 15 | "homepage": "https://www.dereuromark.de", 16 | "role": "Maintainer" 17 | }, 18 | { 19 | "name": "Other contributors", 20 | "homepage": "https://github.com/dereuromark/cakephp-qrcode/graphs/contributors", 21 | "role": "Developer" 22 | } 23 | ], 24 | "support": { 25 | "issues": "https://github.com/dereuromark/cakephp-qrcode/issues", 26 | "source": "https://github.com/dereuromark/cakephp-qrcode/" 27 | }, 28 | "require": { 29 | "php": ">=8.1", 30 | "cakephp/cakephp": "^5.1.1", 31 | "chillerlan/php-qrcode": "^5.0" 32 | }, 33 | "require-dev": { 34 | "fig-r/psr2r-sniffer": "dev-master", 35 | "phpunit/phpunit": "^10.5 || ^11.5 || ^12.1" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "QrCode\\": "src/", 40 | "QrCode\\Test\\Fixture\\": "tests/Fixture/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "QrCode\\Test\\": "tests/", 46 | "Cake\\Test\\": "vendor/cakephp/cakephp/tests/", 47 | "TestApp\\": "tests/test_app/src/" 48 | } 49 | }, 50 | "prefer-stable": true, 51 | "scripts": { 52 | "test": "phpunit", 53 | "test-coverage": "phpunit --log-junit tmp/coverage/unitreport.xml --coverage-html tmp/coverage --coverage-clover tmp/coverage/coverage.xml", 54 | "stan": "phpstan analyse", 55 | "stan-tests": "phpstan analyse -c tests/phpstan.neon", 56 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^2.0.0 && mv composer.backup composer.json", 57 | "lowest": "validate-prefer-lowest", 58 | "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", 59 | "cs-check": "phpcs --extensions=php", 60 | "cs-fix": "phpcbf --extensions=php" 61 | }, 62 | "config": { 63 | "allow-plugins": { 64 | "cakephp/plugin-installer": true, 65 | "dealerdirect/phpcodesniffer-composer-installer": true 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /config/app.example.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'formatter' => \QrCode\Utility\Formatter::class, 6 | ], 7 | ]; 8 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # QrCode Plugin docs 2 | 3 | ## Helper usage 4 | 5 | Add the helper in your `AppView` class: 6 | ```php 7 | $this->loadHelper('QrCode.QrCode'); 8 | ``` 9 | Then you can use it in your views to display QR codes: 10 | ```php 11 | echo $this->QrCode->image($text, $optionalOptions); 12 | ``` 13 | 14 | By default, it uses base64encoded images, so no 2nd request is required. 15 | 16 | ### Formatter 17 | You can use the built-in formatter for most common QR code types: 18 | - Text 19 | - Url 20 | - Phone Number (Call) 21 | - Text message (SMS) 22 | - eMail 23 | - Geo Coordinates (Maps) 24 | - Wifi Network 25 | - Market 26 | - Vcard 27 | 28 | ```php 29 | $text = $this->Qrcode->formatter()->formatText($text); 30 | $geo = $this->Qrcode->formatter()->formatGeo($lat, $lng); 31 | $sms = $this->Qrcode->formatter()->formatSms($number, $text); 32 | ... 33 | 34 | echo $this->QrCode->image(...); 35 | ``` 36 | It will help to normalize user input coming from forms or alike. 37 | 38 | ### Controller rendering 39 | If you want more control over the image, as well as type (png, svg), you 40 | can also let the controller action render it and use it as a generated 41 | on-the-fly image. 42 | 43 | ```php 44 | echo $this->QrCode->svg($content, $options); 45 | // or 46 | echo $this->QrCode->png($content, $options); 47 | ``` 48 | 49 | ### Advanced usage 50 | 51 | See also https://php-qrcode.readthedocs.io/en/v5.0.x/Usage/Advanced-usage.html 52 | 53 | ## Admin Backend 54 | Go to `/admin/qr-code`. 55 | 56 | Make sure you set up ACL to only have admins access this part. 57 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | config/ 7 | src/ 8 | tests/ 9 | /tests/test_files/ 10 | /tests/test_app/ 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Controller/Admin/QrCodeController.php: -------------------------------------------------------------------------------- 1 | request->is('post')) { 21 | $result = $this->request->getData('content'); 22 | } 23 | 24 | $this->set(compact('result', 'options')); 25 | } 26 | 27 | /** 28 | * @return \Cake\Http\Response|null|void 29 | */ 30 | public function image() { 31 | $content = $this->request->getQuery('content'); 32 | 33 | $result = $this->formatter()->formatText($content); 34 | $options = []; 35 | 36 | if ($this->request->getParam('_ext') === 'png') { 37 | $options['outputType'] = QROutputInterface::GDIMAGE_PNG; 38 | } 39 | 40 | $this->set(compact('result', 'options')); 41 | } 42 | 43 | /** 44 | * @return \QrCode\Utility\FormatterInterface 45 | */ 46 | public function formatter(): FormatterInterface { 47 | /** @var class-string $className */ 48 | $className = Configure::read('QrCode.formatter') ?? Formatter::class; 49 | 50 | return new $className(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Controller/QrCodeController.php: -------------------------------------------------------------------------------- 1 | request->getQuery('content'); 27 | 28 | $result = $this->formatter()->formatText($content); 29 | $options = []; 30 | 31 | if ($this->request->getParam('_ext') === 'png') { 32 | $options['outputType'] = QROutputInterface::GDIMAGE_PNG; 33 | } 34 | 35 | $this->set(compact('result', 'options')); 36 | } 37 | 38 | /** 39 | * @return \QrCode\Utility\FormatterInterface 40 | */ 41 | public function formatter(): FormatterInterface { 42 | /** @var class-string $className */ 43 | $className = Configure::read('QrCode.formatter') ?? Formatter::class; 44 | 45 | return new $className(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/QrCodePlugin.php: -------------------------------------------------------------------------------- 1 | addExtensions(['svg', 'png']); 44 | 45 | $routes->plugin( 46 | 'QrCode', 47 | function (RouteBuilder $builder) { 48 | $builder->fallbacks(); 49 | }, 50 | ); 51 | 52 | $routes->prefix('Admin', function (RouteBuilder $builder) { 53 | $builder->plugin( 54 | 'QrCode', 55 | function (RouteBuilder $builder) { 56 | $builder->connect('/', ['controller' => 'QrCode', 'action' => 'index']); 57 | 58 | $builder->fallbacks(); 59 | }, 60 | ); 61 | }); 62 | 63 | parent::routes($routes); 64 | } 65 | 66 | /** 67 | * Add middleware for the plugin. 68 | * 69 | * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. 70 | * 71 | * @return \Cake\Http\MiddlewareQueue 72 | */ 73 | public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { 74 | return $middlewareQueue; 75 | } 76 | 77 | /** 78 | * Add commands for the plugin. 79 | * 80 | * @param \Cake\Console\CommandCollection $commands The command collection to update. 81 | * 82 | * @return \Cake\Console\CommandCollection 83 | */ 84 | public function console(CommandCollection $commands): CommandCollection { 85 | return $commands; 86 | } 87 | 88 | /** 89 | * Register application container services. 90 | * 91 | * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection 92 | * 93 | * @param \Cake\Core\ContainerInterface $container The Container to update. 94 | * 95 | * @return void 96 | */ 97 | public function services(ContainerInterface $container): void { 98 | // Add your services here 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/Utility/Config.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | protected array $types = [ 13 | 'text' => 'Text', 14 | 'url' => 'Url', 15 | 'tel' => 'Phone Number', 16 | 'sms' => 'Text message', 17 | 'email' => 'E-Mail', 18 | 'geo' => 'Geo', 19 | 'wifi' => 'Wifi Network', 20 | 'market' => 'Market', 21 | 'card' => 'Vcard', 22 | ]; 23 | 24 | /** 25 | * @return array 26 | */ 27 | public function types(): array { 28 | return $this->types; 29 | } 30 | 31 | /** 32 | * @param string $text 33 | * @param string|null $type 34 | * 35 | * @return string 36 | */ 37 | public function formatText(string $text, ?string $type = null): string { 38 | switch ($type) { 39 | case 'text': 40 | break; 41 | case 'url': 42 | $text = Router::url($text, true); 43 | 44 | break; 45 | case 'tel': 46 | $text = 'tel:' . $text; 47 | 48 | break; 49 | case 'email': 50 | $text = 'mailto:' . $text; 51 | 52 | break; 53 | case 'market': 54 | $text = 'market://search?q=pname:' . $text; 55 | } 56 | 57 | return $text; 58 | } 59 | 60 | /** 61 | * @param array $content 62 | * 63 | * @return string 64 | */ 65 | public function formatCard(array $content): string { 66 | if (is_array($content['birthday'])) { 67 | $content['birthday'] = $content['birthday']['year'] . '-' . $content['birthday']['month'] . '-' . $content['birthday']['day']; 68 | } 69 | 70 | $res = []; 71 | foreach ($content as $key => $val) { 72 | switch ($key) { 73 | case 'name': 74 | $res[] = 'N:' . $val; # //TODO: support array 75 | 76 | break; 77 | case 'nickname': 78 | $res[] = 'NICKNAME:' . $val; 79 | 80 | break; 81 | case 'sound': 82 | $res[] = 'SOUND:' . $val; 83 | 84 | break; 85 | case 'note': 86 | $val = str_replace(';', ',', $val); 87 | $res[] = 'NOTE:' . $val; //TODO: remove other invalid characters 88 | 89 | break; 90 | case 'birthday': 91 | if (strlen($val) !== 8) { 92 | $val = substr($val, 0, 4) . substr($val, 6, 2) . substr($val, 10, 2); 93 | } 94 | $res[] = 'BDAY:' . $val; 95 | 96 | break; 97 | case 'tel': 98 | $val = (array)$val; 99 | foreach ($val as $v) { 100 | $res[] = 'TEL:' . $v; 101 | } 102 | 103 | break; 104 | case 'video': 105 | $val = (array)$val; 106 | foreach ($val as $v) { 107 | $res[] = 'TEL-AV:' . $v; 108 | } 109 | 110 | break; 111 | case 'address': 112 | $val = (array)$val; 113 | foreach ($val as $v) { 114 | $res[] = 'ADR:' . $v; //TODO: reformat (array etc) 115 | } 116 | 117 | break; 118 | case 'org': 119 | $val = (array)$val; 120 | foreach ($val as $v) { 121 | $res[] = 'ORG:' . $v; 122 | } 123 | 124 | break; 125 | case 'role': 126 | $val = (array)$val; 127 | foreach ($val as $v) { 128 | $res[] = 'ROLE:' . $v; 129 | } 130 | 131 | break; 132 | case 'email': 133 | $val = (array)$val; 134 | foreach ($val as $v) { 135 | $res[] = 'EMAIL:' . $v; 136 | } 137 | 138 | break; 139 | case 'url': 140 | $val = (array)$val; 141 | foreach ($val as $v) { 142 | $res[] = 'URL:' . Router::url($v, true); 143 | } 144 | 145 | break; 146 | } 147 | } 148 | 149 | return 'MECARD:' . implode(';', $res) . ';'; 150 | } 151 | 152 | /** 153 | * @param string $network 154 | * @param string $password 155 | * @param string|null $type 156 | * 157 | * @return string 158 | */ 159 | public function formatWifi(string $network, string $password, ?string $type = null): string { 160 | if ($type === null) { 161 | $type = 'WPA'; 162 | } 163 | 164 | $options = [ 165 | 'T:' . $type, 166 | 'S:' . $network, 167 | 'P:' . $password, 168 | ]; 169 | 170 | return 'WIFI:' . implode(';', $options) . ';;'; 171 | } 172 | 173 | /** 174 | * @param string $number 175 | * @param string $content 176 | * 177 | * @return string 178 | */ 179 | public function formatSms(string $number, string $content): string { 180 | return 'smsto:' . $number . ':' . $content; 181 | } 182 | 183 | /** 184 | * @param float $lat 185 | * @param float $lng 186 | * 187 | * @return string 188 | */ 189 | public function formatGeo(float $lat, float $lng): string { 190 | return 'geo:' . implode(',', [$lat, $lng]); // like 77.1,11.8 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/Utility/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | $content 20 | * 21 | * @return string 22 | */ 23 | public function formatCard(array $content): string; 24 | 25 | /** 26 | * @param string $number 27 | * @param string $content 28 | * 29 | * @return string 30 | */ 31 | public function formatSms(string $number, string $content): string; 32 | 33 | /** 34 | * @param float $lat 35 | * @param float $lng 36 | * 37 | * @return string 38 | */ 39 | public function formatGeo(float $lat, float $lng): string; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/View/Helper/QrCodeHelper.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected array $_defaultConfig = [ 31 | 'colorMap' => [], 32 | 'formatter' => Formatter::class, 33 | ]; 34 | 35 | /** 36 | * @return \QrCode\Utility\FormatterInterface 37 | */ 38 | public function formatter(): FormatterInterface { 39 | /** @var class-string $className */ 40 | $className = $this->getConfig('formatter'); 41 | 42 | return new $className(); 43 | } 44 | 45 | /** 46 | * Base64 encoded image directly returned. 47 | * 48 | * @param string $content 49 | * @param array $options 50 | * 51 | * @return string 52 | */ 53 | public function image(string $content, array $options = []): string { 54 | $options = $this->normalizeOptions($options); 55 | 56 | $qrcode = (new QRCode(new QROptions($options)))->render($content); 57 | 58 | return sprintf('QR Code', $qrcode); 59 | } 60 | 61 | /** 62 | * SVG image from controller rendering. 63 | * 64 | * Make sure the action is allowed/accessible. 65 | * 66 | * @param string $content 67 | * @param array $options 68 | * 69 | * @return string 70 | */ 71 | public function svg(string $content, array $options = []): string { 72 | $url = $this->Url->build(['plugin' => 'QrCode', 'controller' => 'QrCode', 'action' => 'image', '_ext' => Config::TYPE_SVG, '?' => ['content' => $content] + $options]); 73 | 74 | return sprintf('QR Code', $url); 75 | } 76 | 77 | /** 78 | * PNG image from controller rendering. 79 | * 80 | * Make sure the action is allowed/accessible. 81 | * 82 | * @param string $content 83 | * @param array $options 84 | * 85 | * @return string 86 | */ 87 | public function png(string $content, array $options = []): string { 88 | $url = $this->Url->build(['plugin' => 'QrCode', 'controller' => 'QrCode', 'action' => 'image', '_ext' => Config::TYPE_PNG, '?' => ['content' => $content] + $options]); 89 | 90 | return sprintf('QR Code', $url); 91 | } 92 | 93 | /** 94 | * Raw image display. 95 | * 96 | * @internal 97 | * 98 | * @param string $content 99 | * @param array $options 100 | * 101 | * @return string 102 | */ 103 | public function raw(string $content, array $options = []): string { 104 | $options = $this->normalizeOptions($options); 105 | 106 | $options['outputBase64'] = false; 107 | 108 | return (new QRCode(new QROptions($options)))->render($content); 109 | } 110 | 111 | /** 112 | * Get Imagick resource for further customization. 113 | * 114 | * @internal 115 | * 116 | * @param string $content 117 | * @param array $options 118 | * 119 | * @return object 120 | */ 121 | public function resource(string $content, array $options = []): object { 122 | $options = $this->normalizeOptions($options); 123 | 124 | $options['outputBase64'] = false; 125 | $options['outputType'] = QROutputInterface::IMAGICK; 126 | $options['returnResource'] = true; 127 | 128 | return (new QRCode(new QROptions($options)))->render($content); 129 | } 130 | 131 | /** 132 | * @param array $options 133 | * 134 | * @return array 135 | */ 136 | protected function normalizeOptions(array $options): array { 137 | $options += [ 138 | 'version' => Version::AUTO, // to avoid code length issues 139 | 'scale' => 3, 140 | 'margin' => 0, 141 | 'imageBase64' => true, 142 | 'transparent' => false, 143 | 'level' => 'L', 144 | 'connectPaths' => true, 145 | ]; 146 | 147 | switch ($options['level']) { 148 | case 'M': 149 | $options['eccLevel'] = EccLevel::M; 150 | 151 | break; 152 | case 'Q': 153 | $options['eccLevel'] = EccLevel::Q; 154 | 155 | break; 156 | case 'H': 157 | $options['eccLevel'] = EccLevel::H; 158 | 159 | break; 160 | default: 161 | $options['eccLevel'] = EccLevel::L; 162 | } 163 | 164 | $options = [ 165 | 'imageTransparent' => $options['transparent'], 166 | 'addQuietzone' => $options['margin'] > 0, 167 | ] + $options; 168 | 169 | return $options; 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/View/QrCodePngView.php: -------------------------------------------------------------------------------- 1 | request->getParam('_ext') ?: 'png'; 24 | /** @var string $contentType */ 25 | $contentType = $this->response->getMimeType($ext); 26 | 27 | $response = $this->getResponse(); 28 | $responseType = $response->getHeaderLine('Content-Type'); 29 | if ($responseType === '' || str_starts_with($responseType, 'text/html')) { 30 | $response = $response->withType($contentType); 31 | } 32 | $this->setResponse($response); 33 | } 34 | 35 | /** 36 | * Default config options. 37 | * 38 | * @var array 39 | */ 40 | protected array $_defaultConfig = [ 41 | 'ext' => 'png', 42 | ]; 43 | 44 | /** 45 | * @var array 46 | */ 47 | protected array $mimeTypes = [ 48 | Config::TYPE_SVG => 'image/svg+xml', 49 | Config::TYPE_PNG => 'image/png', 50 | ]; 51 | 52 | /** 53 | * Constructor 54 | * 55 | * @param \Cake\Http\ServerRequest|null $request Request instance. 56 | * @param \Cake\Http\Response|null $response Response instance. 57 | * @param \Cake\Event\EventManager|null $eventManager Event manager instance. 58 | * @param array $viewOptions View options. See View::$_passedVars for list of 59 | * options which get set as class properties. 60 | * 61 | * @throws \Cake\Core\Exception\CakeException 62 | */ 63 | public function __construct( 64 | ?ServerRequest $request = null, 65 | ?Response $response = null, 66 | ?EventManager $eventManager = null, 67 | array $viewOptions = [], 68 | ) { 69 | parent::__construct($request, $response, $eventManager, $viewOptions); 70 | 71 | $this->layout = ''; 72 | $this->disableAutoLayout(); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/View/QrCodeView.php: -------------------------------------------------------------------------------- 1 | request->getParam('_ext') ?: 'svg'; 25 | /** @var string $contentType */ 26 | $contentType = $this->response->getMimeType($ext); 27 | 28 | $response = $this->getResponse(); 29 | $responseType = $response->getHeaderLine('Content-Type'); 30 | if ($responseType === '' || str_starts_with($responseType, 'text/html')) { 31 | $response = $response->withType($contentType); 32 | } 33 | $this->setResponse($response); 34 | 35 | parent::initialize(); 36 | } 37 | 38 | /** 39 | * Default config options. 40 | * 41 | * @var array 42 | */ 43 | protected array $_defaultConfig = [ 44 | 'ext' => 'svg', 45 | ]; 46 | 47 | /** 48 | * @var array 49 | */ 50 | protected array $mimeTypes = [ 51 | Config::TYPE_SVG => 'image/svg+xml', 52 | Config::TYPE_PNG => 'image/png', 53 | ]; 54 | 55 | /** 56 | * Constructor 57 | * 58 | * @param \Cake\Http\ServerRequest|null $request Request instance. 59 | * @param \Cake\Http\Response|null $response Response instance. 60 | * @param \Cake\Event\EventManager|null $eventManager Event manager instance. 61 | * @param array $viewOptions View options. See View::$_passedVars for list of 62 | * options which get set as class properties. 63 | * 64 | * @throws \Cake\Core\Exception\CakeException 65 | */ 66 | public function __construct( 67 | ?ServerRequest $request = null, 68 | ?Response $response = null, 69 | ?EventManager $eventManager = null, 70 | array $viewOptions = [], 71 | ) { 72 | parent::__construct($request, $response, $eventManager, $viewOptions); 73 | 74 | $this->layout = ''; 75 | $this->disableAutoLayout(); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /templates/Admin/QrCode/image.php: -------------------------------------------------------------------------------- 1 | QrCode->raw($result, $options); 9 | -------------------------------------------------------------------------------- /templates/Admin/QrCode/index.php: -------------------------------------------------------------------------------- 1 | 8 | 15 |
16 | 17 |

18 | 19 |

By default, we render a simple SVG image that can be scaled up/down easily.

20 | 21 |
22 | Result'; 25 | echo $this->QrCode->image($result, $options); 26 | } 27 | ?> 28 |
29 | 30 |

Generate QR Code

31 | 32 | Form->create();?> 33 |

Enter some text (URL, ...)

34 | 35 | Form->control('content', ['autocomplete' => 'off', 'type' => 'textarea']); 37 | ?> 38 |
39 | Form->button(__('Go'), ['class' => 'btn btn-success']);?> 40 |
41 | Form->end(); ?> 42 | 43 | 44 |
45 | -------------------------------------------------------------------------------- /templates/QrCode/image.php: -------------------------------------------------------------------------------- 1 | QrCode->raw($result, $options); 9 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Admin/QrCodeControllerTest.php: -------------------------------------------------------------------------------- 1 | session([ 23 | 'Auth' => [ 24 | 'User' => [ 25 | 'id' => 1, 26 | ], 27 | ], 28 | ]); 29 | 30 | $this->get(['prefix' => 'Admin', 'plugin' => 'QrCode', 'controller' => 'QrCode', 'action' => 'index']); 31 | 32 | $this->assertResponseOk(); 33 | } 34 | 35 | /** 36 | * @uses \QrCode\Controller\Admin\QrCodeController::image() 37 | * 38 | * @return void 39 | */ 40 | public function testImage(): void { 41 | $this->disableErrorHandlerMiddleware(); 42 | 43 | $this->session([ 44 | 'Auth' => [ 45 | 'User' => [ 46 | 'id' => 1, 47 | ], 48 | ], 49 | ]); 50 | 51 | $this->get(['prefix' => 'Admin', 'plugin' => 'QrCode', 'controller' => 'QrCode', 'action' => 'image', '_ext' => 'svg', '?' => ['content' => 'Foo Bar']]); 52 | 53 | $this->assertResponseOk(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/QrCodeControllerTest.php: -------------------------------------------------------------------------------- 1 | disableErrorHandlerMiddleware(); 25 | 26 | $this->session([ 27 | 'Auth' => [ 28 | 'User' => [ 29 | 'id' => 1, 30 | ], 31 | ], 32 | ]); 33 | 34 | $this->get(['plugin' => 'QrCode', 'controller' => 'QrCode', 'action' => 'image', '_ext' => 'svg', '?' => ['content' => 'Foo Bar']]); 35 | 36 | $this->assertResponseOk(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/TestCase/Utility/FormatterTest.php: -------------------------------------------------------------------------------- 1 | formatter = new Formatter($view); 24 | 25 | $this->loadRoutes(); 26 | } 27 | 28 | /** 29 | * @return void 30 | */ 31 | protected function tearDown(): void { 32 | unset($this->formatter); 33 | 34 | parent::tearDown(); 35 | } 36 | 37 | /** 38 | * @uses \QrCode\View\Helper\QrCodeHelper::image() 39 | * 40 | * @return void 41 | */ 42 | public function testWifi(): void { 43 | $network = 'FooBar'; 44 | $result = $this->formatter->formatWifi($network, 'pwd'); 45 | $this->assertSame('WIFI:T:WPA;S:FooBar;P:pwd;;', $result); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/QrCodeHelperTest.php: -------------------------------------------------------------------------------- 1 | QrCode = new QrCodeHelper($view); 25 | 26 | $this->loadRoutes(); 27 | } 28 | 29 | /** 30 | * @return void 31 | */ 32 | protected function tearDown(): void { 33 | unset($this->QrCode); 34 | 35 | parent::tearDown(); 36 | } 37 | 38 | /** 39 | * @uses \QrCode\View\Helper\QrCodeHelper::image() 40 | * 41 | * @return void 42 | */ 43 | public function testImage(): void { 44 | $content = 'Foo Bar'; 45 | $image = $this->QrCode->image($content); 46 | $this->assertNotEmpty($image); 47 | $this->assertStringStartsWith('QR Code'; 60 | $this->assertSame($expected, $image); 61 | } 62 | 63 | /** 64 | * @uses \QrCode\View\Helper\QrCodeHelper::svg() 65 | * 66 | * @return void 67 | */ 68 | public function testResource(): void { 69 | $this->skipIf(!class_exists(Imagick::class), 'Imagick not available'); 70 | 71 | $content = 'Foo Bar'; 72 | $image = $this->QrCode->resource($content); 73 | $this->assertInstanceOf(Imagick::class, $image); 74 | 75 | $this->assertNotEmpty($image->getImageBlob()); 76 | } 77 | 78 | } 79 | --------------------------------------------------------------------------------