Claude Console – instalacja cakePHP

PHP nie ma klienta AI – zapytania poprzez curl

Klucz API generujemy w Claude Console i przechowujemy zaszyfrowany w bazie danych

ALTER TABLE brands ADD anthropic_api_key_enc text DEFAULT NULL COMMENT "anthropic_api_key_enc";

.env – klucz szyfrujący  (32 bajty = 256 bitów):

# php -r "echo base64_encode(random_bytes(32));"
config/app_local.php
// Szyfrowanie kluczy API i innych wrażliwych danych
'Encryption' => [
     'key' => 'meDX61dddsfasrfLMUwTxdfsdadfad=',
],

Serwis szyfrujący

// src/Service/EncryptionService.php 

namespace App\Service; 

use Cake\Core\Configure; 
use RuntimeException; 

class EncryptionService
{
    private string $key;
    private string $cipher = 'aes-256-gcm';

    public function __construct()
    {
        $raw = base64_decode(env('APP_ENCRYPTION_KEY', ''));

        if (strlen($raw) !== 32) {
            throw new RuntimeException('APP_ENCRYPTION_KEY musi mieć 32 bajty (base64).');
        }

        $this->key = $raw;
    }

    public function encrypt(string $plaintext): string
    {
        $ivLen = openssl_cipher_iv_length($this->cipher);
        $iv    = random_bytes($ivLen);
        $tag   = '';

        $encrypted = openssl_encrypt($plaintext, $this->cipher, $this->key, 0, $iv, $tag);

        // Zapisujemy IV + tag + ciphertext razem (base64)
        return base64_encode($iv . $tag . $encrypted);
    }

    public function decrypt(string $stored): string
    {
        $decoded = base64_decode($stored);
        $ivLen   = openssl_cipher_iv_length($this->cipher);
        $tagLen  = 16; // GCM tag zawsze 16 bajtów

        $iv        = substr($decoded, 0, $ivLen);
        $tag       = substr($decoded, $ivLen, $tagLen);
        $encrypted = substr($decoded, $ivLen + $tagLen);

        $plaintext = openssl_decrypt($encrypted, $this->cipher, $this->key, 0, $iv, $tag);

        if ($plaintext === false) {
            throw new RuntimeException('Odszyfrowanie nie powiodło się — zły klucz lub dane uszkodzone.');
        }

        return $plaintext;
    }
}

xxx

// src/Service/AnthropicService.php

namespace App\Service;

use Cake\Core\Configure;

class AnthropicService
{
    private string $apiKey;
    private string $apiUrl = 'https://api.anthropic.com/v1/messages';
    private string $model  = 'claude-sonnet-4-20250514';

    public function __construct()
    {
        $this->apiKey = Configure::read('Anthropic.apiKey');
    }

    public function ask(string $userMessage, string $systemPrompt = ''): string
    {
        $payload = [
            'model'      => $this->model,
            'max_tokens' => 1024,
            'messages'   => [
                ['role' => 'user', 'content' => $userMessage],
            ],
        ];

        if ($systemPrompt !== '') {
            $payload['system'] = $systemPrompt;
        }

        $ch = curl_init($this->apiUrl);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_HTTPHEADER     => [
                'x-api-key: ' . $this->apiKey,
                'anthropic-version: 2023-06-01',
                'content-type: application/json',
            ],
            CURLOPT_POSTFIELDS => json_encode($payload),
        ]);

        $body     = curl_exec($ch);
        $response = json_decode($body, true);
        curl_close($ch);

        return $response['content'][0]['text'] ?? '';
    }
}

Użycie w kontrolerze

// src/Controller/ChatController.php

namespace App\Controller;

use App\Service\AnthropicService;

class ChatController extends AppController
{
    public function index(): void
    {
        $anthropic = new AnthropicService();

        $answer = $anthropic->ask(
            'Jakie są zalety CakePHP 5?',
            'Jesteś ekspertem PHP. Odpowiadaj zwięźle po polsku.'
        );

        $this->set(compact('answer'));
    }
}

Authentication – podstawowe komendy

W szablonie:

<?php
  $user_id = $this->getRequest()->getAttribute('identity')->getIdentifier();
?>

W kontrolerze:

$user_id = $this->Authentication->getIdentity()->getIdentifier();

$role = $this->Authentication->getIdentity()->get('role');

$role == 'admin' || 'user'

Wyłączenie z dostępu po zalogowaniu

use Cake\Event\EventInterface;


public function beforeFilter(EventInterface $event): void
{
        parent::beforeFilter($event);

        if(!$this->is_admin) {
            $this->Flash->error(__('Nie ma takiej strony'));
            $event->setResult($this->redirect('/'));
            return;
        }

        $this->Authentication->addUnauthenticatedActions(['edit', 'changePassword' ]);
    }

Aktualizacja URL

let url = new URL(location.href);

$("#proj-tabs").find(".nav-link").on('click', function() {

    var tab_name = $(this).attr('data-bs-target').replace("#", "");

    url.searchParams.set("s_tab", tab_name);

    history.replaceState(null, null, url.href);

});

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'

Czytaj dalej NipTrait