NipTrait

validateNip($nip)   - Sprawdza poprawność     - Invoice::validateNip('1234567890') → true 
normalizeNip($nip)  - Usuwa myślniki, spacje  - Invoice::normalizeNip('123-456-78-90') → '1234567890'
formatNip($nip)     - Formatuje XXX-XXX-XX-XX - Invoice::formatNip('1234567890') → '123-456-78-90'
generateRandomNip() - Generuje losowy - NIP   - Invoice::generateRandomNip() → '857-234-19-23'
getTestNip()        - Stały NIP testowy       - Invoice::getTestNip() → '123-456-78-16'
isForeignNip($nip)  - Czy NIP zagraniczny     - Invoice::isForeignNip('9912345678') → true 
getNipInfo($nip)    - Debug info Zwraca tablicę z detailami 

seller_nip_formatted - Virtual field - $invoice->seller_nip_formatted → '123-456-78-90' 
buyer_nip_formatted  - Virtual field - $invoice->buyer_nip_formatted → '123-456-78-90'

użycie w formularzach:

<!-- Formularz automatycznie normalizuje nip przy zapisie -->
<?= $this->Form->control('seller_nip', [
  'label' => 'NIP Sprzedawcy',
  'placeholder' => '123-456-78-90',
  'help' => 'Można wpisać z myślnikami lub bez'
]) ?>

<!-- Użytkownik wpisuje: "123-456-78-90" -->
<!-- Zapisuje się: "1234567890" -->
<!-- Wyświetla się: "123-456-78-90" -->

Encje:

use App\Model\Entity\Trait\NipTrait;

class Invoice extends Entity { 

   use NipTrait; 
.....

Tabela – walidacja

use App\Model\Entity\Invoice;
......

$validator 
    ->scalar('seller_nip') 
    ->maxLength('seller_nip', 10) 
    ->minLength('seller_nip', 10) 
    ->requirePresence('seller_nip', 'create') 
    ->notEmptyString('seller_nip') 
    ->numeric('seller_nip', 'NIP musi zawierać tylko cyfry') 
    ->add('seller_nip', 'validNip', [ 
        'rule' => function ($value, $context) { 
            return Invoice::validateNip($value); // ← UŻYJ TRAITA 
         }, 
        'message' => 'Nieprawidłowy NIP sprzedawcy' 
    ]);

Wyświetlanie – szablon view.php

<!-- Automatyczne formatowanie (virtual field) -->
<p>NIP Sprzedawcy: <?= h($invoice->seller_nip_formatted) ?></p>
<!-- Wyświetli: "123-456-78-90" -->

<!-- Ręczne formatowanie -->
<p>NIP: <?= \App\Model\Entity\Invoice::formatNip($nip) ?></p>

Użycie w kontrolerze:

// Walidacja NIP przed zapisem
if (!Invoice::validateNip($data['seller_nip'])) {
$this->Flash->error('Nieprawidłowy NIP!');
return;
}

// Normalizacja (automatycznie przez setter)
$invoice->seller_nip = '123-456-78-90';
// Zapisze się jako: "1234567890"

// Sprawdź czy zagraniczny
if (Invoice::isForeignNip($invoice->buyer_nip)) {
// To firma zagraniczna
}

Trait – definicja

<?php
declare(strict_types=1);
namespace App\Model\Entity\Trait;

/**
 * NIP Trait
 *
 * Obsługa polskich numerów NIP:
 * - Walidacja poprawności
 * - Normalizacja (usuwanie myślników, spacji)
 * - Formatowanie (XXX-XXX-XX-XX)
 * - Generowanie przykładowych NIP-ów
 *
 * Użycie: w Invoice.php, PurchaseInvoice.php, Brand.php, Customer.php, etc.
 */

trait NipTrait

{
    /**
     * Waliduje numer NIP
     *
     * @param string|null $nip NIP do walidacji
     * @return bool
     */

    public static function validateNip(?string $nip): bool
    {
        if (empty($nip)) {
            return false;
        }

        // Normalizuj - usuń wszystko oprócz cyfr
        $nip = self::normalizeNip($nip);

        // NIP musi mieć dokładnie 10 cyfr
        if (strlen($nip) !== 10) {
            return false;
        }

        // NIP nie może zaczynać się od 0
        if ($nip[0] === '0') {
            return false;
        }

        // Algorytm walidacji NIP (suma kontrolna)
        $weights = [6, 5, 7, 2, 3, 4, 5, 6, 7];
        $sum = 0;

        for ($i = 0; $i < 9; $i++) {
            $sum += (int)$nip[$i] * $weights[$i];
        }

        $checksum = $sum % 11;

        // Jeśli suma kontrolna = 10, NIP jest nieprawidłowy
        if ($checksum === 10) {
            return false;
        }

        // Sprawdź czy ostatnia cyfra się zgadza
        return $checksum == (int)$nip[9];
    }

    /**
     * Normalizuje NIP - usuwa wszystkie znaki oprócz cyfr
     *
     * @param string|null $nip NIP z myślnikami, spacjami, etc.
     * @return string NIP tylko cyfry (np. "1234567890")
     */
    public static function normalizeNip(?string $nip): string
    {
        if (empty($nip)) {
            return '';
        }

        // Usuń wszystko oprócz cyfr
        return preg_replace('/[^0-9]/', '', $nip);
    }

    /**
     * Formatuje NIP do postaci XXX-XXX-XX-XX
     *
     * @param string|null $nip NIP (z myślnikami lub bez)
     * @return string Sformatowany NIP (np. "123-456-78-90")
     */
    public static function formatNip(?string $nip): string
    {
        if (empty($nip)) {
            return '';
        }

        // Normalizuj
        $nip = self::normalizeNip($nip);

        // Jeśli nie ma 10 cyfr, zwróć oryginalny
        if (strlen($nip) !== 10) {
            return $nip;
        }

        // Formatuj: XXX-XXX-XX-XX
        returnsubstr($nip,0,3).'-'.
               substr($nip,3,3).'-'.
               substr($nip,6,2).'-'.
               substr($nip, 8, 2);
    }

    /**
     * Generuje losowy, POPRAWNY numer NIP (dla testów)
     *
     * @return string Wygenerowany NIP w formacie XXX-XXX-XX-XX
     */
    public static function generateRandomNip(): string
    {
        // Wygeneruj losowe 9 cyfr (pierwsza nie może być 0)
        $nip = (string)rand(1, 9);  // Pierwsza cyfra: 1-9
     
        for ($i = 1; $i < 9; $i++) {
            $nip .= (string)rand(0, 9);
        }

        // Oblicz cyfrę kontrolną
        $weights = [6, 5, 7, 2, 3, 4, 5, 6, 7];
        $sum = 0;

        for ($i = 0; $i < 9; $i++) {
            $sum += (int)$nip[$i] * $weights[$i];
        }
        $checksum = $sum % 11;

        // Jeśli checksum = 10, generuj ponownie
        if ($checksum === 10) {
            return self::generateRandomNip();
        }

        // Dodaj cyfrę kontrolną
        $nip .= (string)$checksum;

        // Zwróć sformatowany
        return self::formatNip($nip);
    }

    /**
     * Generuje przykładowy NIP dla firmy testowej
     * (zawsze ten sam, poprawny NIP)
     *
     * @return string "123-456-78-16" (poprawny NIP testowy)
     */
    public static function getTestNip(): string
    {
        return '123-456-78-16';  // Poprawny NIP testowy
    }

    /**
     * Generuje tablicę przykładowych NIP-ów (dla seedów)
     *
     * @param int $count Liczba NIP-ów do wygenerowania
     * @return array<string>
     */
    public static function generateMultipleNips(int $count = 10): array
    {
        $nips = [];
       
        for ($i = 0; $i < $count; $i++) {
            $nips[] = self::generateRandomNip();
        }
       
        return $nips;
    }

    /**
     * Pobiera sformatowany NIP dla danej encji
     * (używane jako virtual field)
     *
     * @param string $field Nazwa pola (np. 'seller_nip', 'buyer_nip')
     * @return string|null
     */
    protected function getFormattedNip(string $field): ?string
    {
        if (empty($this->{$field})) {
            return null;
        }
        return self::formatNip($this->{$field});
    }

    /**
     * Virtual field: Sformatowany NIP sprzedawcy
     * (dla Invoice i PurchaseInvoice)
     *
     * @return string|null
     */
    protected function _getSellerNipFormatted(): ?string
    {
        return $this->getFormattedNip('seller_nip');
    }

    /**
     * Virtual field: Sformatowany NIP nabywcy
     * (dla Invoice i PurchaseInvoice)
     *
     * @return string|null
     */
    protected function _getBuyerNipFormatted(): ?string
    {
        return $this->getFormattedNip('buyer_nip');
    }

    /**
     * Setter: Automatyczna normalizacja NIP przy zapisie
     * (wywołuje się automatycznie przy $entity->seller_nip = '123-456-78-90')
     *
     * @param string|null $value
     * @return string|null
     */
    protected function _setSellerNip(?string $value): ?string
    {
        return $value ? self::normalizeNip($value) : null;
    }

    /**
     * Setter: Automatyczna normalizacja NIP nabywcy
     *
     * @param string|null $value
     * @return string|null
     */
    protected function _setBuyerNip(?string $value): ?string
    {
        return $value ? self::normalizeNip($value) : null;
    }

    /**
     * Sprawdza czy NIP należy do podmiotu zagranicznego
     * (NIP zagraniczny zaczyna się od "99")
     *
     * @param string|null $nip
     * @return bool
     */
    public static function isForeignNip(?string $nip): bool
    {
        if (empty($nip)) {
            return false;
        }

        $normalized = self::normalizeNip($nip);
        return substr($normalized, 0, 2) === '99';
    }

    /**
     * Pobiera informacje o NIP-ie (helper do debugowania)
     *
     * @param string|null $nip
     * @return array
     */
    public static function getNipInfo(?string $nip): array
    {
        $normalized = self::normalizeNip($nip);
       
        return [
            'original'   => $nip,
            'normalized' => $normalized,
            'formatted'  => self::formatNip($nip),
            'valid'      => self::validateNip($nip),
            'length'     => strlen($normalized),
            'is_foreign' => self::isForeignNip($nip),
        ];
    }
} // end NipTrait

W testach:

// Wygeneruj losowy NIP
$testNip = Invoice::generateRandomNip(); // "857-234-19-23"

// Pobierz stały NIP testowy
$nip = Invoice::getTestNip(); // "123-456-78-16"

// Wygeneruj wiele NIP-ów
$nips = Invoice::generateMultipleNips(50);
// ['123-456-78-16', '857-234-19-23', ...]

// Debug info
$info = Invoice::getNipInfo('123-456-78-90');
/*
[
'original' => '123-456-78-90',
'normalized' => '1234567890',
'formatted' => '123-456-78-90',
'valid' => true,
'length' => 10,
'is_foreign' => false,
]
*/