Passed
Pull Request — master (#121)
by Manuel
09:56 queued 01:17
created

QrBill::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 8
c 3
b 0
f 0
dl 0
loc 10
rs 10
cc 1
nc 1
nop 1
1
<?php declare(strict_types=1);
2
3
namespace Sprain\SwissQrBill;
4
5
use Endroid\QrCode\ErrorCorrectionLevel;
6
use Sprain\SwissQrBill\Constraint\ValidCreditorInformationPaymentReferenceCombination;
7
use Sprain\SwissQrBill\DataGroup\AddressInterface;
8
use Sprain\SwissQrBill\DataGroup\Element\AdditionalInformation;
9
use Sprain\SwissQrBill\DataGroup\Element\AlternativeScheme;
10
use Sprain\SwissQrBill\DataGroup\Element\CreditorInformation;
11
use Sprain\SwissQrBill\DataGroup\Element\Header;
12
use Sprain\SwissQrBill\DataGroup\Element\PaymentAmountInformation;
13
use Sprain\SwissQrBill\DataGroup\Element\PaymentReference;
14
use Sprain\SwissQrBill\DataGroup\EmptyElement\EmptyAdditionalInformation;
15
use Sprain\SwissQrBill\DataGroup\EmptyElement\EmptyAddress;
16
use Sprain\SwissQrBill\DataGroup\QrCodeableInterface;
17
use Sprain\SwissQrBill\Exception\InvalidQrBillDataException;
18
use Sprain\SwissQrBill\QrCode\QrCode;
19
use Sprain\SwissQrBill\String\StringModifier;
20
use Sprain\SwissQrBill\Validator\SelfValidatableInterface;
21
use Sprain\SwissQrBill\Validator\SelfValidatableTrait;
22
use Symfony\Component\Validator\Constraints as Assert;
23
use Symfony\Component\Validator\Mapping\ClassMetadata;
24
25
final class QrBill implements SelfValidatableInterface
26
{
27
    use SelfValidatableTrait;
28
29
    private const SWISS_CROSS_LOGO_FILE = __DIR__ . '/../assets/swiss-cross.optimized.png';
30
    private const PX_QR_CODE = 543;    // recommended 46x46 mm in px @ 300dpi – in pixel based outputs, the final image size may be slightly different, depending on the qr code contents
31
    private const PX_SWISS_CROSS = 83; // recommended 7x7 mm in px @ 300dpi
32
33
    private Header $header;
34
    private ?CreditorInformation $creditorInformation;
35
    private ?AddressInterface $creditor;
36
    private ?PaymentAmountInformation $paymentAmountInformation;
37
    private ?AddressInterface $ultimateDebtor;
38
    private ?PaymentReference $paymentReference;
39
    private ?AdditionalInformation $additionalInformation;
40
    /** @var AlternativeScheme[] */
41
    private array $alternativeSchemes;
42
43
    private function __construct(Header $header)
44
    {
45
        $this->header = $header;
46
        $this->creditorInformation = null;
47
        $this->creditor = null;
48
        $this->paymentAmountInformation = null;
49
        $this->ultimateDebtor = null;
50
        $this->paymentReference = null;
51
        $this->additionalInformation = null;
52
        $this->alternativeSchemes = [];
53
    }
54
55
    public static function create(): self
56
    {
57
        $header = Header::create(
58
            Header::QRTYPE_SPC,
59
            Header::VERSION_0200,
60
            Header::CODING_LATIN
61
        );
62
63
        return new self($header);
64
    }
65
66
    public function getHeader(): Header
67
    {
68
        return $this->header;
69
    }
70
71
    public function setHeader(Header $header): self
72
    {
73
        $this->header = $header;
74
75
        return $this;
76
    }
77
78
    public function getCreditorInformation(): ?CreditorInformation
79
    {
80
        return $this->creditorInformation;
81
    }
82
83
    public function setCreditorInformation(CreditorInformation $creditorInformation): self
84
    {
85
        $this->creditorInformation = $creditorInformation;
86
87
        return $this;
88
    }
89
90
    public function getCreditor(): ?AddressInterface
91
    {
92
        return $this->creditor;
93
    }
94
95
    public function setCreditor(AddressInterface $creditor): self
96
    {
97
        $this->creditor = $creditor;
98
        
99
        return $this;
100
    }
101
102
    public function getPaymentAmountInformation(): ?PaymentAmountInformation
103
    {
104
        return $this->paymentAmountInformation;
105
    }
106
107
    public function setPaymentAmountInformation(PaymentAmountInformation $paymentAmountInformation): self
108
    {
109
        $this->paymentAmountInformation = $paymentAmountInformation;
110
        
111
        return $this;
112
    }
113
114
    public function getUltimateDebtor(): ?AddressInterface
115
    {
116
        return $this->ultimateDebtor;
117
    }
118
119
    public function setUltimateDebtor(AddressInterface $ultimateDebtor): self
120
    {
121
        $this->ultimateDebtor = $ultimateDebtor;
122
        
123
        return $this;
124
    }
125
126
    public function getPaymentReference(): ?PaymentReference
127
    {
128
        return $this->paymentReference;
129
    }
130
131
    public function setPaymentReference(PaymentReference $paymentReference): self
132
    {
133
        $this->paymentReference = $paymentReference;
134
        
135
        return $this;
136
    }
137
138
    public function getAdditionalInformation(): ?AdditionalInformation
139
    {
140
        return $this->additionalInformation;
141
    }
142
143
    public function setAdditionalInformation(AdditionalInformation $additionalInformation): self
144
    {
145
        $this->additionalInformation = $additionalInformation;
146
147
        return $this;
148
    }
149
150
    public function getAlternativeSchemes(): array
151
    {
152
        return $this->alternativeSchemes;
153
    }
154
155
    public function setAlternativeSchemes(array $alternativeSchemes): self
156
    {
157
        $this->alternativeSchemes = $alternativeSchemes;
158
159
        return $this;
160
    }
161
162
    public function addAlternativeScheme(AlternativeScheme $alternativeScheme): self
163
    {
164
        $this->alternativeSchemes[] = $alternativeScheme;
165
166
        return $this;
167
    }
168
169
    /**
170
     * @throws InvalidQrBillDataException
171
     */
172
    public function getQrCode(): QrCode
173
    {
174
        if (!$this->isValid()) {
175
            throw new InvalidQrBillDataException(
176
                'The provided data is not valid to generate a qr code. Use getViolations() to find details.'
177
            );
178
        }
179
180
        $qrCode = new QrCode();
181
        $qrCode->setText($this->getQrCodeContent());
182
        $qrCode->setSize(self::PX_QR_CODE);
183
        $qrCode->setLogoHeight(self::PX_SWISS_CROSS);
184
        $qrCode->setLogoWidth(self::PX_SWISS_CROSS);
185
        $qrCode->setLogoPath(self::SWISS_CROSS_LOGO_FILE);
186
        $qrCode->setRoundBlockSize(true, \Endroid\QrCode\QrCode::ROUND_BLOCK_SIZE_MODE_ENLARGE);
187
        $qrCode->setMargin(0);
188
        $qrCode->setErrorCorrectionLevel(new ErrorCorrectionLevel(ErrorCorrectionLevel::MEDIUM));
189
190
        return $qrCode;
191
    }
192
193
    private function getQrCodeContent(): string
194
    {
195
        $elements = [
196
            $this->getHeader(),
197
            $this->getCreditorInformation(),
198
            $this->getCreditor(),
199
            new EmptyAddress(), # Placeholder for ultimateCreditor, which is currently not yet allowed to be used by the implementation guidelines
200
            $this->getPaymentAmountInformation(),
201
            $this->getUltimateDebtor() ?: new EmptyAddress(),
202
            $this->getPaymentReference(),
203
            $this->getAdditionalInformation() ?: new EmptyAdditionalInformation(),
204
            $this->getAlternativeSchemes()
205
        ];
206
207
        $qrCodeStringElements = $this->extractQrCodeDataFromElements($elements);
208
209
        return implode("\n", $qrCodeStringElements);
210
    }
211
212
    private function extractQrCodeDataFromElements(array $elements): array
213
    {
214
        $qrCodeElements = [];
215
216
        foreach ($elements as $element) {
217
            if ($element instanceof QrCodeableInterface) {
218
                $qrCodeElements = array_merge($qrCodeElements, $element->getQrCodeData());
219
            } elseif (is_array($element)) {
220
                $qrCodeElements = array_merge($qrCodeElements, $this->extractQrCodeDataFromElements($element));
221
            }
222
        }
223
224
        array_walk($qrCodeElements, function (&$string) {
225
            if (is_string($string)) {
226
                $string = StringModifier::replaceLineBreaksWithString($string);
227
                $string = StringModifier::replaceMultipleSpacesWithOne($string);
228
                $string = trim($string);
229
            }
230
        });
231
232
        return $qrCodeElements;
233
    }
234
235
    public static function loadValidatorMetadata(ClassMetadata $metadata): void
236
    {
237
        $metadata->addConstraint(
238
            new ValidCreditorInformationPaymentReferenceCombination()
239
        );
240
241
        $metadata->addPropertyConstraints('header', [
242
            new Assert\NotNull(),
243
            new Assert\Valid()
244
        ]);
245
246
        $metadata->addPropertyConstraints('creditorInformation', [
247
            new Assert\NotNull(),
248
            new Assert\Valid()
249
        ]);
250
251
        $metadata->addPropertyConstraints('creditor', [
252
            new Assert\NotNull(),
253
            new Assert\Valid()
254
        ]);
255
256
        $metadata->addPropertyConstraints('paymentAmountInformation', [
257
            new Assert\NotNull(),
258
            new Assert\Valid()
259
        ]);
260
261
        $metadata->addPropertyConstraints('ultimateDebtor', [
262
            new Assert\Valid()
263
        ]);
264
265
        $metadata->addPropertyConstraints('paymentReference', [
266
            new Assert\NotNull(),
267
            new Assert\Valid()
268
        ]);
269
270
        $metadata->addPropertyConstraints('alternativeSchemes', [
271
            new Assert\Count([
272
                'max' => 2
273
            ]),
274
            new Assert\Valid([
275
                'traverse' => true
276
            ])
277
        ]);
278
    }
279
}
280