Skip to content

Commit 34820bd

Browse files
committed
📖
1 parent 4176e35 commit 34820bd

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# Custom `QROutputInterface`
2+
3+
Let's suppose that you want to create your own output interface because there's no built-in output class that supports the format you need for your application.
4+
In this example we'll create a string output class that outputs the coordinates for each module, separated by module type.
5+
6+
7+
## Class skeleton
8+
9+
We'll start with a skeleton that extends `QROutputAbstract` and implements the methods that are required by `QROutputInterface`:
10+
11+
```php
12+
class MyCustomOutput extends QROutputAbstract{
13+
14+
public static function moduleValueIsValid($value):bool{}
15+
16+
protected function prepareModuleValue($value){}
17+
18+
protected function getDefaultModuleValue(bool $isDark){}
19+
20+
public function dump(string $file = null){}
21+
22+
}
23+
```
24+
25+
26+
## Module values
27+
28+
The validator should check whether the given input value and range is valid for the output class and if it can be given to the `QROutputAbstract::prepareModuleValue()` method.
29+
For example in the built-in GD output it would check if the value is an array that has a minimum of 3 elements (for RGB), each of which is numeric.
30+
31+
In this example we'll accept string values, the characters `a-z` (case-insensitive) and a hyphen `-`:
32+
33+
```php
34+
public static function moduleValueIsValid($value):bool{
35+
return is_string($value) && preg_match('/^[a-z-]+$/i', $value) === 1;
36+
}
37+
```
38+
39+
To prepare the final module substitute, you should transform the given (validated) input value in a way so that it can be accessed without any further calls or transformation.
40+
In the built-in output for example this means it would return an `ImagickPixel` instance or the integer value returned by `imagecolorallocate()` on the current `GdImage` instance.
41+
42+
For our example, we'll lowercase the validated string:
43+
44+
```php
45+
protected function prepareModuleValue($value):string{
46+
return strtolower($value);
47+
}
48+
```
49+
50+
Finally, we need to provide a default value for dark and light, you can call `prepareModuleValue()` here if necessary.
51+
We'll return an empty string `''` here as we're going to use the `QROutputInterface::LAYERNAMES` constant for non-existing values
52+
(returning `null` would run into an exception in `QROutputAbstract::getModuleValue()`).
53+
54+
```php
55+
protected function getDefaultModuleValue(bool $isDark):string{
56+
return '';
57+
}
58+
```
59+
60+
61+
## Transform the output
62+
63+
In our example, we want to collect the modules by type and have the collections listed under a header for each type.
64+
In order to do so, we need to collect the modules per `$M_TYPE` before we can render the final output.
65+
66+
```php
67+
public function dump(string $file = null):string{
68+
$collections = [];
69+
70+
// loop over the matrix and collect the modules per layer
71+
foreach($this->matrix->getMatrix() as $y => $row){
72+
foreach($row as $x => $M_TYPE){
73+
$collections[$M_TYPE][] = $this->module($x, $y, $M_TYPE);
74+
}
75+
}
76+
77+
// build the final output
78+
$out = [];
79+
80+
foreach($collections as $M_TYPE => $collection){
81+
$name = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
82+
// the section header
83+
$out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
84+
// the list of modules
85+
$out[] = sprintf("%s\n", implode("\n", $collection));
86+
}
87+
88+
return implode("\n", $out);
89+
}
90+
```
91+
92+
We've introduced another method that handles the module rendering, which incooperates handling of the `QROptions::$drawLightModules` setting:
93+
94+
```php
95+
protected function module(int $x, int $y, int $M_TYPE):string{
96+
97+
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
98+
return '';
99+
}
100+
101+
return sprintf('x: %s, y: %s', $x, $y);
102+
}
103+
```
104+
105+
Speaking of option settings, there's also `QROptions::$connectPaths` which we haven't taken care of yet - the good news is that we don't need to as it is already implemented!
106+
We'll modify the above `dump()` method to use `QROutputAbstract::collectModules()` instead.
107+
108+
The module collector accepts a closure as its only parameter, the closure is called with 4 parameters:
109+
110+
- `$x` : current column
111+
- `$y` : current row
112+
- `$M_TYPE` : field value
113+
- `$M_TYPE_LAYER`: (possibly modified) field value that acts as layer id
114+
115+
We'll only need the first 3 parameters, so our closure would look as follows:
116+
117+
```php
118+
$closure = fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE);
119+
```
120+
121+
As of PHP 8.1+ we can narrow this down with the [first class callable syntax](https://www.php.net/manual/en/functions.first_class_callable_syntax.php):
122+
123+
```php
124+
$closure = $this->module(...);
125+
```
126+
127+
This is our final output method then:
128+
129+
```php
130+
public function dump(string $file = null):string{
131+
$collections = $this->collectModules($this->module(...));
132+
133+
// build the final output
134+
$out = [];
135+
136+
foreach($collections as $M_TYPE => $collection){
137+
$name = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
138+
// the section header
139+
$out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
140+
// the list of modules
141+
$out[] = sprintf("%s\n", implode("\n", $collection));
142+
}
143+
144+
return implode("\n", $out);
145+
}
146+
```
147+
148+
149+
## Run the custom output
150+
151+
To run the output we just need to set the `QROptions::$outputInterface` to our custom class:
152+
153+
```php
154+
$options = new QROptions;
155+
$options->outputType = QROutputInterface::CUSTOM;
156+
$options->outputInterface = MyCustomOutput::class;
157+
$options->connectPaths = true;
158+
$options->drawLightModules = true;
159+
160+
// our custom module values
161+
$options->moduleValues = [
162+
QRMatrix::M_DATA => 'these-modules-are-light',
163+
QRMatrix::M_DATA_DARK => 'here-is-a-dark-module',
164+
];
165+
166+
$qrcode = new QRCode($options);
167+
$qrcode->addByteSegment('test');
168+
169+
var_dump($qrcode->render());
170+
```
171+
172+
The output looks similar to the following:
173+
```
174+
these-modules-are-light (000000000010)
175+
176+
x: 0, y: 0
177+
x: 1, y: 0
178+
x: 2, y: 0
179+
...
180+
181+
here-is-a-dark-module (100000000010)
182+
183+
x: 4, y: 4
184+
x: 5, y: 4
185+
x: 6, y: 4
186+
...
187+
```
188+
189+
Profit!
190+
191+
192+
## Summary
193+
194+
We've learned how to create a custom output class for a string based format similar to several of the built-in formats such as SVG or EPS.
195+
196+
The full code of our custom class below:
197+
198+
```php
199+
class MyCustomOutput extends QROutputAbstract{
200+
201+
protected function prepareModuleValue($value):string{
202+
return strtolower($value);
203+
}
204+
205+
protected function getDefaultModuleValue(bool $isDark):string{
206+
return '';
207+
}
208+
209+
public static function moduleValueIsValid($value):bool{
210+
return is_string($value) && preg_match('/^[a-z-]+$/i', $value) === 1;
211+
}
212+
213+
public function dump(string $file = null):string{
214+
$collections = $this->collectModules($this->module(...));
215+
216+
// build the final output
217+
$out = [];
218+
219+
foreach($collections as $M_TYPE => $collection){
220+
$name = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
221+
// the section header
222+
$out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
223+
// the list of modules
224+
$out[] = sprintf("%s\n", implode("\n", $collection));
225+
}
226+
227+
return implode("\n", $out);
228+
}
229+
230+
protected function module(int $x, int $y, int $M_TYPE):string{
231+
232+
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
233+
return '';
234+
}
235+
236+
return sprintf('x: %s, y: %s', $x, $y);
237+
}
238+
239+
}
240+
```

docs/Readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ The markdown sources for the [Read the Docs online manual](https://php-qrcode.re
6868

6969
- [Module values](./Customizing/Module-Values.md)
7070
- [`QROutputAbstract`](./Customizing/QROutputAbstract.md)
71+
- [Custom `QROutputInterface`](./Customizing/Custom-output-interface.md)
7172

7273

7374
### Built-In Output Modules

docs/Usage/Overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ For the QR Code reader, either `ext-gd` or `ext-imagick` is required!
6060
- [twill](https://github.com/area17/twill)
6161
- [Elefant CMS](https://github.com/jbroadway/elefant)
6262
- [OSIRIS](https://github.com/JKoblitz/osiris)
63+
- [EspoCRM](https://github.com/espocrm/espocrm)
6364
- Articles:
6465
- [Twilio: How to Create a QR Code in PHP](https://www.twilio.com/blog/create-qr-code-in-php) (featuring v4.3.x)
6566

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ This work is licensed under the Creative Commons Attribution 4.0 International (
2828

2929
Customizing/Module-Values.md
3030
Customizing/QROutputAbstract.md
31+
Customizing/Custom-output-interface.md
3132

3233
.. toctree::
3334
:maxdepth: 3

0 commit comments

Comments
 (0)