first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Ignora Variáveis de ambiente
|
||||
.env
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Auth\Controllers;
|
||||
|
||||
use KrothiumAPI\Utils\HttpUtil;
|
||||
use WorkbloomERP\Utils\SanitizeUtil;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
|
||||
|
||||
class AuthController {
|
||||
/**
|
||||
* Endpoint de API para autenticação de usuário (**Login**).
|
||||
*
|
||||
* Este método gerencia a interface HTTP (POST) para validar as credenciais do usuário.
|
||||
* Ele atua como uma camada fina (*Thin Controller*), responsável por capturar o
|
||||
* payload da requisição, sanitizar os dados de entrada para prevenir injeções
|
||||
* e delegar a verificação de identidade e gestão de sessão/tokens para o `AuthService`.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Captura e Validação:** Extrai o corpo da requisição. Se o payload estiver vazio, interrompe com erro 400 (Bad Request).
|
||||
* 2. **Sanitização:** Aplica `SanitizeUtil::string()` nos campos de login e senha para garantir a limpeza dos dados antes do processamento de negócio.
|
||||
* 3. **Processamento de Negócio:** Invoca `AuthService::login()`, que realiza a consulta de credenciais.
|
||||
* 4. **Normalização de Resposta:**
|
||||
* - **Sucesso (200 OK):** Retorna os dados de sessão (ex: token de acesso).
|
||||
* - **Tratamento de Erros:** Captura `AppException` (credenciais inválidas, conta bloqueada, etc.), formatando a resposta para que o frontend receba um objeto de erro estruturado.
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - A responsabilidade de verificar a hash da senha e gerar o token de acesso reside exclusivamente no `AuthService`, isolando a lógica de segurança do controlador.
|
||||
* - Utiliza `HttpUtil` para garantir consistência no formato da resposta JSON, independentemente do sucesso ou falha da autenticação.
|
||||
*
|
||||
* @return void O método encerra o processamento enviando uma resposta HTTP JSON ao cliente.
|
||||
* @see AuthService::login() Para os detalhes sobre a regra de autenticação e geração de tokens.
|
||||
*/
|
||||
public function login(): void {
|
||||
try {
|
||||
$form = HttpUtil::getRequestBody(form_type: 'POST');
|
||||
if (empty($form)) {
|
||||
throw new AppException(message: 'Requisição inválida.', code: 400);
|
||||
}
|
||||
|
||||
$authService = ServiceFactory::makeAuthService();
|
||||
$response = $authService->login(login: SanitizeUtil::string(value: $form['login']), senha: SanitizeUtil::string(value: $form['senha']));
|
||||
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: $response['response_code'] ?? 200,
|
||||
message: $response['message'] ?? 'Login realizado com sucesso.',
|
||||
output: $response['output'] ?? null
|
||||
);
|
||||
} catch(AppException $e) {
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: $e->getCode() ?? 500,
|
||||
message: $e->getMessage(),
|
||||
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint de API para a seleção de contexto de empresa ativa.
|
||||
*
|
||||
* Este método gerencia a interface HTTP (POST) para permitir que um usuário
|
||||
* autenticado alterne sua empresa ativa no sistema multi-empresa. O controlador
|
||||
* realiza a captura do payload, sanitiza o UUID da empresa e delega a
|
||||
* complexa lógica de troca de sessão e regeneração de tokens ao `AuthService`.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Captura e Validação:** Extrai o corpo da requisição. Se o payload estiver vazio ou malformado, retorna erro 400 (Bad Request).
|
||||
* 2. **Sanitização:** Aplica `SanitizeUtil::string()` no `empresa_uuid` para garantir a integridade dos dados recebidos.
|
||||
* 3. **Processamento de Negócio:** Invoca `AuthService::selectCompany()`, que valida o acesso à empresa, gera um novo JWT e registra a sessão.
|
||||
* 4. **Normalização de Resposta:**
|
||||
* - **Sucesso (200 OK):** Retorna o novo token de acesso contendo o contexto da empresa selecionada.
|
||||
* - **Tratamento de Erros:** Captura `AppException` (ex: empresa inexistente, falta de permissão), retornando uma resposta JSON estruturada com os detalhes do erro para o cliente.
|
||||
*
|
||||
* ---
|
||||
* ## Arquitetura de Seleção de Contexto
|
||||
*
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - Segue o padrão "Thin Controller", onde a orquestração de infraestrutura (cache, banco de dados, JWT) é encapsulada na camada de serviço.
|
||||
* - Este endpoint é fundamental para aplicações multi-tenant ou multi-empresa, permitindo que o usuário altere seu "escopo" de atuação sem precisar realizar um re-login completo.
|
||||
*
|
||||
* @return void O método encerra o processamento enviando uma resposta HTTP JSON ao cliente.
|
||||
* @see AuthService::selectCompany() Para os detalhes sobre a regra de negócio aplicada na troca de contexto.
|
||||
*/
|
||||
public function selectCompany(): void {
|
||||
try {
|
||||
$form = HttpUtil::getRequestBody(form_type: 'POST');
|
||||
if (empty($form)) {
|
||||
throw new AppException(message: 'Requisição inválida.', code: 400);
|
||||
}
|
||||
|
||||
$authService = ServiceFactory::makeAuthService();
|
||||
$response = $authService->selectCompany(empresa_uuid: SanitizeUtil::string(value: $form['empresa_uuid']));
|
||||
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: $response['response_code'] ?? 200,
|
||||
message: $response['message'] ?? 'Empresa selecionada com sucesso.',
|
||||
output: $response['output'] ?? null
|
||||
);
|
||||
} catch(AppException $e) {
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: $e->getCode() ?? 500,
|
||||
message: $e->getMessage(),
|
||||
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Auth\Factories;
|
||||
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Module\v0\Auth\Services\AuthService;
|
||||
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioSessionRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioEmpresaRepo;
|
||||
|
||||
class ServiceFactory {
|
||||
public static function makeAuthService() {
|
||||
$db = new DBService();
|
||||
return new AuthService(
|
||||
db: $db,
|
||||
usuarioRepo: new UsuarioRepo(db: $db),
|
||||
empresaRepo: new EmpresaRepo(db: $db),
|
||||
usuarioSessionRepo: new UsuarioSessionRepo(db: $db),
|
||||
usuarioEmpresaRepo: new UsuarioEmpresaRepo(db: $db),
|
||||
);
|
||||
}
|
||||
|
||||
public static function makeUsuarioSessionRepo() {
|
||||
$db = new DBService();
|
||||
return new UsuarioSessionRepo(
|
||||
db: $db
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Auth\Middlewares;
|
||||
|
||||
use WorkbloomERP\Utils\JwtUtil;
|
||||
use KrothiumAPI\Utils\HttpUtil;
|
||||
use WorkbloomERP\Utils\CacheUtil;
|
||||
use WorkbloomERP\Utils\CypherUtil;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
|
||||
|
||||
class AuthMiddleware {
|
||||
/**
|
||||
* Valida se o token JWT da requisição atual corresponde a um estado de "Login Inicial".
|
||||
*
|
||||
* Este método atua como um guardião de segurança (Guard) para rotas que exigem
|
||||
* ações obrigatórias do usuário logo após o primeiro acesso ao sistema, como a
|
||||
* troca obrigatória de senha ou a configuração de MFA (Autenticação de Dois Fatores).
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Extração:** Obtém o token JWT do cabeçalho `Authorization: Bearer`.
|
||||
* 2. **Validação:** Verifica a integridade e assinatura do token através do `JwtUtil`.
|
||||
* 3. **Verificação de Claim:** Decodifica o token e confirma se a claim `initial_login` está presente e definida como `true`.
|
||||
* 4. **Retorno:**
|
||||
* - Retorna `true` se o token for válido e corresponder ao estado de login inicial.
|
||||
* - Retorna um `array` estruturado contendo detalhes do erro caso a validação falhe.
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - O método utiliza variáveis de ambiente (`$_ENV`) para configurar o `JwtUtil`, garantindo que as configurações de `secretKey`, `algorithm` e `ttl` estejam sincronizadas com a política de segurança da aplicação.
|
||||
* - Este método captura internamente instâncias de `AppException`, tratando o fluxo de erro de forma silenciosa para o chamador, retornando um status de erro estruturado em vez de propagar a exceção.
|
||||
*
|
||||
* @return true|array{status: string, response_code: int, message: string}
|
||||
* Retorna `true` em caso de sucesso. Retorna um array com o erro caso o token seja inválido, ausente ou não possua a claim necessária.
|
||||
*/
|
||||
public static function isInitialLogin(): true|array {
|
||||
try {
|
||||
// Valida o token JWT presente no header Authorization (Bearer Token)
|
||||
|
||||
$bearerToken = HttpUtil::getBearerToken();
|
||||
if (!$bearerToken) {
|
||||
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
|
||||
}
|
||||
|
||||
$jwtUtil = new JwtUtil(
|
||||
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
|
||||
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
|
||||
);
|
||||
$decodedToken = $jwtUtil->validate(token: $bearerToken);
|
||||
if (!$decodedToken) {
|
||||
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
|
||||
}
|
||||
|
||||
$decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$userData = $decodedToken['user_data'] ?? null;
|
||||
if (!$userData) {
|
||||
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
|
||||
}
|
||||
|
||||
if (!isset($userData['initial_login']) || $userData['initial_login'] !== true) {
|
||||
throw new AppException(message: 'Token de acesso não corresponde a um login inicial.', code: 403);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(AppException $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'response_code' => $e->getCode() ?? 500,
|
||||
'message' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware de autenticação para validar sessões e tokens JWT em requisições protegidas.
|
||||
*
|
||||
* Este método atua como um "Gatekeeper" (Guard). Ele implementa uma estratégia híbrida
|
||||
* de validação (Cache-First, seguida de DB e JWT) para equilibrar performance com
|
||||
* segurança. Ele verifica a existência do token, a validade da assinatura JWT,
|
||||
* a revogação da sessão no banco de dados e a integridade do dispositivo (IP/User-Agent).
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Extração:** Obtém o token do header `Authorization`. Retorna erro 401 se ausente.
|
||||
* 2. **Cache-First Check:** Verifica no Redis (`login:main:{token}`). Se encontrado, autoriza imediatamente (retorna `null`), evitando carga no banco de dados.
|
||||
* 3. **Validação de Persistência:** Consulta o `UsuarioSessionRepo` (via hash do token). Valida revogação (`revoked_at`) e integridade.
|
||||
* 4. **Validação JWT:** Decodifica e verifica a assinatura/expiração (`exp`) do JWT.
|
||||
* 5. **Validação de Integridade (Guard):** Invoca `validateSessionData` para garantir que o IP e User-Agent atuais coincidem com a sessão original.
|
||||
* 6. **Caching:** Se tudo estiver correto, armazena o payload no cache para acelerar futuras requisições e retorna `null` (acesso concedido).
|
||||
*
|
||||
* ---
|
||||
* ## Arquitetura de Validação
|
||||
*
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - **Interface de Retorno:**
|
||||
* - `null`: A autenticação foi bem-sucedida; o fluxo de execução pode continuar.
|
||||
* - `array`: A autenticação falhou; o array contém a resposta de erro estruturada para o cliente.
|
||||
* - **Segurança:** Utiliza `CypherUtil` para garantir que tokens sensíveis não sejam comparados em texto puro contra o banco de dados.
|
||||
*
|
||||
* @return array|null Retorna `null` se o usuário estiver autenticado e autorizado. Retorna um `array` com `response_code`, `message` e `output` em caso de erro.
|
||||
* @see JwtUtil::validate() Para detalhes sobre a validação da assinatura criptográfica.
|
||||
* @see self::validateSessionData() Para a verificação de segurança de IP e User-Agent.
|
||||
*/
|
||||
public static function handle(): ?array {
|
||||
try {
|
||||
// Valida o token JWT presente no header Authorization (Bearer Token)
|
||||
$bearerToken = HttpUtil::getBearerToken();
|
||||
if (!$bearerToken) {
|
||||
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
|
||||
}
|
||||
|
||||
$jwtUtil = new JwtUtil(
|
||||
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
|
||||
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
|
||||
);
|
||||
|
||||
// Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
|
||||
$decodedToken = $jwtUtil->validate(token: $bearerToken);
|
||||
if (!$decodedToken) {
|
||||
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
|
||||
}
|
||||
|
||||
// Verifica se o token corresponde a um login inicial, bloqueando o acesso a rotas protegidas caso seja verdadeiro.
|
||||
$decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
if ($decodedToken['user_data']['initial_login'] == true) {
|
||||
throw new AppException(message: 'Acesso negado. O token corresponde a um login inicial e não pode ser usado para acessar esta rota.', code: 403);
|
||||
}
|
||||
|
||||
// Verifica se a sessão principal já está em cache para evitar consultas desnecessárias ao banco de dados e validações repetitivas.
|
||||
// Se a sessão estiver em cache, considera o token como válido e retorna null para permitir o acesso à rota protegida.
|
||||
if (CacheUtil::get(key: "login:main:{$bearerToken}")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$usuarioSessionRepo = ServiceFactory::makeUsuarioSessionRepo();
|
||||
|
||||
// Busca a sessão do usuário no banco de dados utilizando o hash do token para garantir que o token é válido e não foi revogado.
|
||||
$usuarioSessionModel = $usuarioSessionRepo->findByIdentifier(identifier: 'token_hash', value: CypherUtil::hash(data: $bearerToken));
|
||||
if (!$usuarioSessionModel || !CypherUtil::verify(data: $bearerToken, hash: $usuarioSessionModel->getTokenHash()) || $usuarioSessionModel->getRevokedAt()) {
|
||||
throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
|
||||
}
|
||||
|
||||
// Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
|
||||
// Se o token for inválido, expirado ou tiver sua assinatura comprometida, uma exceção é lançada.
|
||||
$decodedToken = $jwtUtil->validate(token: $bearerToken);
|
||||
if (!$decodedToken) {
|
||||
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
|
||||
}
|
||||
|
||||
$decodedToken = json_decode(json: json_encode($decodedToken, JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$remainingTime = $decodedToken['exp'] - time();
|
||||
if ($remainingTime <= 0) {
|
||||
throw new AppException(message: 'Sessão expirada. Faça login novamente.', code: 401);
|
||||
}
|
||||
|
||||
self::validateSessionData(sessionModel: $usuarioSessionModel, tokenData: $decodedToken);
|
||||
CacheUtil::set(key: "login:main:{$bearerToken}", ttl: $remainingTime, value: $decodedToken);
|
||||
return null;
|
||||
} catch (AppException $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'response_code' => $e->getCode() ?: 500,
|
||||
'message' => $e->getMessage(),
|
||||
'output' => $e->getDetails() ? ['errors' => $e->getDetails()] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida a integridade e autenticidade da sessão do usuário comparando metadados.
|
||||
*
|
||||
* Este método atua como uma barreira de segurança (Guard) que garante que a sessão
|
||||
* atual está sendo utilizada pelo mesmo dispositivo e rede que a iniciou. Ao comparar
|
||||
* o IP e o User-Agent da requisição atual com os dados persistidos no banco de dados
|
||||
* (`UsuarioSessionModel`) e os metadados contidos no token JWT (`$tokenData`),
|
||||
* protegemos o sistema contra ataques de sequestro de sessão (session hijacking).
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Captura:** Extrai o IP (`REMOTE_ADDR`) e o User-Agent (`HTTP_USER_AGENT`) da variável superglobal `$_SERVER`.
|
||||
* 2. **Comparação (Match):** Utiliza uma expressão `match` para validar quatro pontos de convergência:
|
||||
* - IP da Sessão (DB) vs. IP Atual.
|
||||
* - User-Agent da Sessão (DB) vs. User-Agent Atual.
|
||||
* - IP registrado no Payload do Token vs. IP Atual.
|
||||
* - User-Agent registrado no Payload do Token vs. User-Agent Atual.
|
||||
* 3. **Bloqueio:** Caso qualquer divergência seja detectada, a sessão é considerada inválida e uma `AppException` (401 Unauthorized) é disparada.
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - O uso de `match` (PHP 8+) torna o código altamente legível e performático para a verificação booleana múltipla.
|
||||
* - Este método é fundamental em cenários onde a segurança de acesso exige que o usuário não troque de rede ou navegador enquanto a sessão estiver ativa.
|
||||
* - Caso a requisição venha através de um proxy, certifique-se de que a configuração de captura de IP (`REMOTE_ADDR`) esteja ajustada corretamente no ambiente.
|
||||
*
|
||||
* @param UsuarioSessionModel $sessionModel O modelo contendo os dados da sessão persistidos no banco.
|
||||
* @param array $tokenData O payload decodificado do token JWT contendo os metadados da sessão.
|
||||
* @return void
|
||||
* @throws AppException Caso haja qualquer incompatibilidade nos dados (IP ou User-Agent), invalidando a sessão.
|
||||
*/
|
||||
private static function validateSessionData(UsuarioSessionModel $sessionModel, array $tokenData): void {
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
|
||||
|
||||
$isInvalidSession = match(true) {
|
||||
$sessionModel->getIpAddress() !== $ipAddress,
|
||||
$sessionModel->getUserAgent() !== $userAgent,
|
||||
$tokenData['session_data']['ip_address'] !== $ipAddress,
|
||||
$tokenData['session_data']['user_agent'] !== $userAgent => true,
|
||||
default => false
|
||||
};
|
||||
if ($isInvalidSession) {
|
||||
throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
use KrothiumAPI\Http\Router;
|
||||
use WorkbloomERP\Module\v0\Auth\Middlewares\AuthMiddleware;
|
||||
use WorkbloomERP\Module\v0\Auth\Controllers\AuthController;
|
||||
|
||||
Router::group(
|
||||
prefix: '/auth',
|
||||
callback: function() {
|
||||
// Rota de autenticação
|
||||
Router::post(uri: '/login', handler: [AuthController::class, 'login']);
|
||||
|
||||
// Rota para seleção de empresa após login inicial
|
||||
Router::post(uri: '/select-company', handler: [AuthController::class, 'selectCompany'], middlewares: [[AuthMiddleware::class, 'isInitialLogin']]);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Auth\Services;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use DateTimeImmutable;
|
||||
use KrothiumAPI\Utils\HttpUtil;
|
||||
use WorkbloomERP\Utils\JwtUtil;
|
||||
use WorkbloomERP\Utils\CacheUtil;
|
||||
use WorkbloomERP\Utils\CypherUtil;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
use WorkbloomERP\Module\v0\Auth\Utils\AuthUtil;
|
||||
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioSessionRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioEmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioEmpresaModel;
|
||||
|
||||
class AuthService {
|
||||
public function __construct(
|
||||
private DBService $db,
|
||||
private UsuarioRepo $usuarioRepo,
|
||||
private EmpresaRepo $empresaRepo,
|
||||
private UsuarioSessionRepo $usuarioSessionRepo,
|
||||
private UsuarioEmpresaRepo $usuarioEmpresaRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Realiza a autenticação de usuário e inicia uma sessão no sistema.
|
||||
*
|
||||
* Este método é o coração do processo de autenticação. Ele valida as credenciais
|
||||
* fornecidas, gera um token JWT (JSON Web Token) para comunicação segura, persiste
|
||||
* a sessão no banco de dados para fins de auditoria/rastreabilidade e armazena
|
||||
* o token no cache (Redis) para validação rápida de requests subsequentes.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Validação de Entrada:** Verifica se `login` e `senha` foram fornecidos.
|
||||
* 2. **Autenticação:** Busca o usuário pelo login e utiliza `password_verify` para validar a hash da senha.
|
||||
* 3. **Geração de Token:** Instancia o `JwtUtil` com configurações de ambiente e gera um novo token JWT.
|
||||
* 4. **Persistência de Sessão (Atomicidade):**
|
||||
* - Insere o registro de sessão na tabela `usuario_session` (contendo dados como IP e User-Agent).
|
||||
* - O hash do token é armazenado para permitir revogação futura.
|
||||
* 5. **Caching:** Armazena o token no Redis com a mesma TTL da expiração do token para otimizar a validação de acesso.
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - **Transacionalidade:** Toda a criação de sessão e persistência ocorre dentro de uma transação de banco de dados, garantindo que não existam sessões órfãs caso o cache ou a criação do token falhem.
|
||||
* - **Segurança:** O token JWT gerado é hasheado (`CypherUtil::hash`) antes de ser salvo no banco para evitar a exposição do token real em caso de vazamento de dados.
|
||||
* - **Infraestrutura:** Requer acesso funcional a: `JWT_SECRET_KEY`, `JWT_ALGORITHM`, `JWT_EXPIRATION_TIME`, `JWT_ISSUER`.
|
||||
*
|
||||
* @param string $login O nome de usuário ou e-mail.
|
||||
* @param string $senha A senha em texto puro fornecida pelo usuário.
|
||||
* @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o código de sucesso e o token JWT.
|
||||
* @throws AppException Caso as credenciais sejam inválidas (401), o payload esteja vazio (400) ou falhas críticas ocorram na geração de token/sessão (500).
|
||||
*/
|
||||
public function login(string $login, string $senha): array {
|
||||
try {
|
||||
// Validação básica de entrada
|
||||
if (empty($login) || empty($senha)) {
|
||||
throw new AppException(message: 'Login e senha são obrigatórios.', code: 400);
|
||||
}
|
||||
|
||||
// Inicia uma transação para garantir a atomicidade das operações de autenticação
|
||||
return $this->db->transaction(
|
||||
callback: function() use ($login, $senha) {
|
||||
$usuarioModel = $this->usuarioRepo->findByLogin(login: $login);
|
||||
if (!$usuarioModel || !password_verify(password: $senha, hash: $usuarioModel->getSenhaHash())) {
|
||||
throw new AppException(message: 'Login ou senha inválidos.', code: 401);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'user_data' => [
|
||||
'initial_login' => true,
|
||||
'uuid' => $usuarioModel->getUuid(),
|
||||
'nome_usuario' => $usuarioModel->getNomeCompleto(),
|
||||
'email' => $usuarioModel->getEmail(),
|
||||
],
|
||||
'exp' => (new DateTimeImmutable())->modify("+5 minutes")->getTimestamp(),
|
||||
];
|
||||
|
||||
$jwtUtil = new JwtUtil(
|
||||
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
|
||||
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
|
||||
);
|
||||
$jwtToken = $jwtUtil->generate(payload: $payload);
|
||||
if (!$jwtToken) {
|
||||
throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
|
||||
}
|
||||
|
||||
// Armazena o token JWT no cache Redis com uma TTL de 10 minutos (600 segundos)
|
||||
CacheUtil::set(key: "login:init:{$jwtToken}", ttl: 600, value: $payload);
|
||||
|
||||
return [
|
||||
'response_code' => 200,
|
||||
'message' => 'Login realizado com sucesso.',
|
||||
'output' => [
|
||||
'tmp_token' => $jwtToken,
|
||||
'empresas' => $this->getEmpresasByUsuarioId($usuarioModel->getId())
|
||||
]
|
||||
];
|
||||
}
|
||||
);
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera e organiza a lista de empresas vinculadas a um determinado usuário.
|
||||
*
|
||||
* Este método realiza o mapeamento entre o ID do usuário e suas respectivas
|
||||
* empresas. Ele filtra as associações através do repositório `usuarioEmpresaRepo`,
|
||||
* hidrata os objetos `EmpresaModel` e organiza os dados em uma estrutura
|
||||
* associativa agrupada pelo nome empresarial.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Busca:** Recupera todos os registros de associação `usuario_id` -> `empresa_id`.
|
||||
* 2. **Hidratação:** Itera sobre as associações, buscando o `EmpresaModel` completo para cada ID encontrado.
|
||||
* 3. **Estruturação:** Monta um array multidimensional onde as chaves são os nomes das empresas e os valores são arrays contendo metadados (UUID, nome, CNPJ, tipo formatado).
|
||||
* 4. **Tratamento de Dados:** Normaliza o campo `tipo` (MATRIZ/FILIAL) para o formato "Capitalizado" (ex: Matriz).
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - Caso o usuário não possua empresas vinculadas ou o repositório retorne vazio, o método retorna um array vazio.
|
||||
* - Ignora silenciosamente qualquer `empresa_id` que não resulte em um `EmpresaModel` válido (falha de integridade referencial).
|
||||
* - O agrupamento pelo nome empresarial facilita o consumo da estrutura pelo frontend.
|
||||
*
|
||||
* @param int $usuario_id O identificador numérico interno do usuário.
|
||||
* @return array<string, array<int, array{uuid: string, nome_empresarial: string, nome_fantasia: string, cnpj: string, tipo: string}>> Array estruturado de empresas.
|
||||
* @throws AppException Caso ocorra falha de acesso aos repositórios.
|
||||
*/
|
||||
private function getEmpresasByUsuarioId(int $usuario_id): array {
|
||||
try {
|
||||
$empresas = $this->usuarioEmpresaRepo->findAllByUsuarioId(usuario_id: $usuario_id);
|
||||
if (empty($empresas)) {
|
||||
return ['Nenhuma empresa vinculada ao usuário.'];
|
||||
}
|
||||
|
||||
$preparedEmpresas = [];
|
||||
foreach($empresas as $i => $empresa) {
|
||||
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'id', value: $empresa['empresa_id']);
|
||||
if (!$empresaModel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$preparedEmpresas[$empresaModel->getNomeEmpresarial()][] = [
|
||||
'uuid' => $empresaModel->getUuid(),
|
||||
'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
|
||||
'nome_fantasia' => $empresaModel->getNomeFantasia(),
|
||||
'cnpj' => $empresaModel->getDocumentCnpj(),
|
||||
'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
|
||||
];
|
||||
}
|
||||
|
||||
return $preparedEmpresas;
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seleciona e define a empresa ativa para o contexto da sessão do usuário.
|
||||
*
|
||||
* Este método é utilizado em sistemas multi-empresa onde o usuário precisa alternar
|
||||
* entre diferentes entidades (empresas) sem realizar um novo login completo.
|
||||
* O método valida se o usuário possui vínculo com a empresa solicitada, gera
|
||||
* um novo token JWT com o contexto da empresa atualizado e registra essa nova
|
||||
* sessão no banco de dados e no cache (Redis).
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Verificação de Sessão:** Garante que o usuário já possui um login ativo (leitura via `AuthUtil`).
|
||||
* 2. **Validação de Acesso:** Verifica se o usuário solicitado possui associação ativa com a empresa (`usuarioEmpresaRepo`).
|
||||
* 3. **Contextualização:** Monta um novo payload contendo os dados do usuário e os dados da empresa selecionada.
|
||||
* 4. **Token Rotation:** Gera um novo token JWT com os novos dados de contexto e define um tempo de expiração (60 minutos).
|
||||
* 5. **Persistência:** Registra a nova sessão no banco de dados (`usuario_session`) e atualiza o estado no cache (`CacheUtil`).
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - **Token Rotation:** O método invalida implicitamente o contexto anterior ao gerar um novo token baseado na nova seleção.
|
||||
* - **Atomicidade:** A operação ocorre dentro de uma transação, garantindo que se o token não for gerado ou a sessão não for criada, nada é alterado.
|
||||
* - **Segurança:** O hash do token gerado é armazenado para permitir rastreabilidade e segurança.
|
||||
*
|
||||
* @param string $empresa_uuid O Identificador Único Universal da empresa que o usuário deseja selecionar.
|
||||
* @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o novo token de acesso com o contexto atualizado.
|
||||
* @throws AppException Caso a sessão seja inválida, a empresa não exista, o usuário não tenha permissão de acesso à empresa ou falhas de persistência ocorram.
|
||||
*/
|
||||
public function selectCompany(string $empresa_uuid) {
|
||||
try {
|
||||
return $this->db->transaction(
|
||||
callback: function() use ($empresa_uuid) {
|
||||
if (empty($empresa_uuid)) {
|
||||
throw new AppException(message: 'UUID da empresa é obrigatório.', code: 400);
|
||||
}
|
||||
|
||||
// Lê os dados da sessão atual para validar a existência de um login prévio e obter o UUID do usuário
|
||||
$sessionData = AuthUtil::readInitSession();
|
||||
if (!$sessionData || !isset($sessionData['user_data']['uuid'])) {
|
||||
throw new AppException(message: 'Sessão de usuário não encontrada. Faça login novamente.', code: 401);
|
||||
}
|
||||
|
||||
// Valida se o usuário tem acesso à empresa selecionada
|
||||
$usuarioModel = $this->usuarioRepo->findByIdentifier(identifier: 'uuid', value: $sessionData['user_data']['uuid']);
|
||||
if (!$usuarioModel) {
|
||||
throw new AppException(message: 'Ocorreu um erro ao selecionar a empresa. Tente novamente mais tarde.', code: 404);
|
||||
}
|
||||
|
||||
// Valida se a empresa existe e se o usuário tem associação com ela
|
||||
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'uuid', value: $empresa_uuid);
|
||||
if (!$empresaModel || !$this->usuarioEmpresaRepo->checkAssociationByUsuarioIdAndEmpresaId(usuario_id: $usuarioModel->getId(), empresa_id: $empresaModel->getId())) {
|
||||
throw new AppException(message: 'Empresa não encontrada.', code: 404);
|
||||
}
|
||||
|
||||
// Monta o payload da sessão principal, incluindo os dados do usuário e da empresa selecionada
|
||||
$payload = [
|
||||
'user_data' => [
|
||||
'initial_login' => false,
|
||||
'uuid' => $usuarioModel->getUuid(),
|
||||
'nome_completo' => $usuarioModel->getNomeCompleto(),
|
||||
'nome_usuario' => $usuarioModel->getNomeUsuario(),
|
||||
'email' => $usuarioModel->getEmail(),
|
||||
'is_root' => $usuarioModel->getIsRoot() ? true : false
|
||||
],
|
||||
'empresa_data' => [
|
||||
'uuid' => $empresaModel->getUuid(),
|
||||
'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
|
||||
'nome_fantasia' => $empresaModel->getNomeFantasia(),
|
||||
'cnpj' => $empresaModel->getDocumentCnpj(),
|
||||
'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
|
||||
],
|
||||
'session_data' => [
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||
],
|
||||
'iat' => (new DateTimeImmutable())->getTimestamp(),
|
||||
'exp' => (new DateTimeImmutable())->modify("+{$_ENV['JWT_EXPIRATION_TIME']} seconds")->getTimestamp(),
|
||||
];
|
||||
|
||||
$jwtUtil = new JwtUtil(
|
||||
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
|
||||
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
|
||||
);
|
||||
$jwtToken = $jwtUtil->generate(payload: $payload);
|
||||
if (!$jwtToken) {
|
||||
throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
|
||||
}
|
||||
|
||||
$usuarioSessionModel = $this->usuarioSessionRepo->insert(
|
||||
new UsuarioSessionModel(
|
||||
uuid: Uuid::uuid7()->toString(),
|
||||
usuario_id: $usuarioModel->getId(),
|
||||
token_hash: CypherUtil::hash(data: $jwtToken),
|
||||
ip_address: $payload['session_data']['ip_address'],
|
||||
user_agent: $payload['session_data']['user_agent'],
|
||||
created_at: new DateTimeImmutable()
|
||||
)
|
||||
);
|
||||
if (!$usuarioSessionModel) {
|
||||
throw new AppException(message: 'Erro ao criar sessão de usuário.', code: 500);
|
||||
}
|
||||
|
||||
if (!CacheUtil::set(key: "login:main:{$jwtToken}", ttl: $_ENV['JWT_EXPIRATION_TIME'], value: $payload)) {
|
||||
throw new AppException(message: 'Erro ao armazenar sessão no cache.', code: 500);
|
||||
}
|
||||
|
||||
$beforeToken = HttpUtil::getBearerToken();
|
||||
if (!CacheUtil::delete(keys: ["login:init:{$beforeToken}"])) {
|
||||
throw new AppException(message: 'Erro ao limpar sessão temporária do cache.', code: 500);
|
||||
}
|
||||
|
||||
return [
|
||||
'response_code' => 200,
|
||||
'message' => 'Empresa selecionada com sucesso.',
|
||||
'output' => [
|
||||
'token' => $jwtToken
|
||||
]
|
||||
];
|
||||
}
|
||||
);
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Auth\Utils;
|
||||
|
||||
use KrothiumAPI\Utils\HttpUtil;
|
||||
use WorkbloomERP\Utils\JwtUtil;
|
||||
use WorkbloomERP\Utils\CacheUtil;
|
||||
use WorkbloomERP\Utils\CryptoUtil;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
|
||||
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
|
||||
|
||||
class AuthUtil {
|
||||
/**
|
||||
* Recupera os dados de uma sessão de "Login Inicial" a partir do cache.
|
||||
*
|
||||
* Este método é utilizado para verificar se existe uma sessão ativa vinculada ao
|
||||
* token de acesso atual no fluxo de "Primeiro Acesso" (Initial Login). Ele permite
|
||||
* identificar rapidamente se o usuário está em um estado que exige ações
|
||||
* obrigatórias, como a alteração de senha inicial ou configuração de MFA,
|
||||
* sem a necessidade de consultar o banco de dados.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Identificação:** Extrai o token Bearer da requisição atual (`HttpUtil::getBearerToken()`).
|
||||
* 2. **Busca em Cache:** Consulta o serviço de cache (Redis) utilizando a chave prefixada `login:init:{token}`.
|
||||
* 3. **Retorno:** Devolve os dados da sessão (array) caso o registro exista e esteja ativo; retorna `null` caso a sessão não exista ou tenha expirado.
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - **Performance:** Este método é altamente performático por consultar exclusivamente a camada de cache, sendo ideal para middlewares de validação de fluxo de login.
|
||||
* - **Segurança:** A chave de busca utiliza o próprio token Bearer, garantindo que o acesso aos dados da sessão inicial seja restrito ao detentor do token correspondente.
|
||||
* - **Integração:** Este método deve ser utilizado em conjunto com o fluxo de autenticação que persiste os dados do login inicial no cache.
|
||||
*
|
||||
* @return array|null Retorna o payload da sessão de login inicial se encontrada; retorna `null` se a sessão não existir no cache.
|
||||
*/
|
||||
public static function readInitSession(): ?array {
|
||||
// Verifica se existe um login inicial
|
||||
$bearerToken = HttpUtil::getBearerToken();
|
||||
$initSession = CacheUtil::get(key: "login:init:{$bearerToken}");
|
||||
if ($initSession) {
|
||||
return $initSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera a sessão principal do usuário ou uma chave específica do payload armazenado em cache.
|
||||
*
|
||||
* Este método centraliza o acesso aos dados de sessão ativos. Ele utiliza o
|
||||
* token Bearer da requisição atual para localizar o payload no cache (Redis) e,
|
||||
* caso uma chave específica seja fornecida, retorna apenas o subconjunto de dados
|
||||
* solicitado, otimizando o acesso às informações de contexto.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Identificação:** Obtém o token de acesso (Bearer Token) da requisição atual via `HttpUtil`.
|
||||
* 2. **Busca:** Consulta o cache usando a chave padronizada `login:main:{token}`.
|
||||
* 3. **Filtragem:**
|
||||
* - Se uma `$key` for fornecida e existir no payload, retorna apenas o valor correspondente (ex: `user_data`).
|
||||
* - Se nenhuma chave for fornecida ou a chave não existir, retorna o payload completo.
|
||||
* 4. **Retorno:** Devolve `null` caso a sessão não esteja em cache (sessão inexistente ou expirada).
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - **Performance:** A consulta ao cache é direta e rápida. O método evita redundância ao permitir filtrar os dados necessários logo na leitura.
|
||||
* - **Flexibilidade:** A inclusão do parâmetro `$key` permite que outros serviços solicitem apenas o bloco de dados que precisam (ex: dados da empresa ou do usuário), reduzindo o processamento desnecessário de arrays grandes.
|
||||
*
|
||||
* @param string $key Chave específica a ser extraída do payload da sessão (opcional; se vazio, retorna o payload completo).
|
||||
* @return array|null Retorna o payload da sessão ou um subconjunto dele; retorna `null` se a sessão não existir no cache.
|
||||
*/
|
||||
public static function readSession(string $key): ?array {
|
||||
$bearerToken = HttpUtil::getBearerToken();
|
||||
$result = CacheUtil::get(key: "login:main:{$bearerToken}");
|
||||
|
||||
if (!$result) {return null;}
|
||||
|
||||
if ($key && isset($result[$key])) {
|
||||
return $result[$key];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Constants;
|
||||
|
||||
class ContribuinteICMSConst {
|
||||
public const CONTATO_CONTRIBUINTE_ICMS = [
|
||||
1 => 'Contribuinte ICMS',
|
||||
2 => 'Contribuinte Isento de Inscrição no Cadastro de Contribuintes',
|
||||
9 => 'Não Contribuinte, que pode ou não possuir Inscrição Estadual no Cadastro de Contribuintes'
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::CONTATO_CONTRIBUINTE_ICMS;
|
||||
}
|
||||
|
||||
public static function isValid(string $value): bool {
|
||||
return array_key_exists(key: $value, array: self::CONTATO_CONTRIBUINTE_ICMS);
|
||||
}
|
||||
|
||||
public static function getDescription(string $key): ?string {
|
||||
return self::CONTATO_CONTRIBUINTE_ICMS[$key] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Constants;
|
||||
|
||||
class NFSeConsumoIbsCbsConst {
|
||||
public const CONTATO_USO_CONSUMO_IBS_CBS = [
|
||||
'1' => 'Sim (Consumidor Final)',
|
||||
'2' => 'Não informar',
|
||||
'0' => 'Não (Operação B2B)',
|
||||
];
|
||||
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::CONTATO_USO_CONSUMO_IBS_CBS;
|
||||
}
|
||||
|
||||
public static function getDescription(string|int|null $key): string {
|
||||
return self::CONTATO_USO_CONSUMO_IBS_CBS[$key] ?? 'Valor desconhecido';
|
||||
}
|
||||
|
||||
public static function isValid(string|int|null $key): bool {
|
||||
return array_key_exists(key: $key, array: self::CONTATO_USO_CONSUMO_IBS_CBS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Constants;
|
||||
|
||||
class OrgaoPublicoConst {
|
||||
public const CONTATO_ORGAO_PUBLICO = [
|
||||
'NAO' => 'Não',
|
||||
'FEDERAL' => 'Órgão Público Federal',
|
||||
'ESTADUAL' => 'Órgão Público Estadual',
|
||||
'MUNICIPAL' => 'Órgão Público Municipal'
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::CONTATO_ORGAO_PUBLICO;
|
||||
}
|
||||
|
||||
public static function isValid(string $value): bool {
|
||||
return array_key_exists(key: $value, array: self::CONTATO_ORGAO_PUBLICO);
|
||||
}
|
||||
|
||||
public static function getDescription(string $key): ?string {
|
||||
return self::CONTATO_ORGAO_PUBLICO[$key] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Constants;
|
||||
|
||||
class PersonalidadeConst {
|
||||
public const CONTATO_PERSONALIDADE = [
|
||||
'PF' => 'Pessoa Física',
|
||||
'PJ' => 'Pessoa Jurídica',
|
||||
'EX' => 'Estrangeiro'
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::CONTATO_PERSONALIDADE;
|
||||
}
|
||||
|
||||
public static function isValid(string $value): bool {
|
||||
return array_key_exists(key: $value, array: self::CONTATO_PERSONALIDADE);
|
||||
}
|
||||
|
||||
public static function getDescription(string $key): ?string {
|
||||
return self::CONTATO_PERSONALIDADE[$key] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Constants;
|
||||
|
||||
class TipoConst {
|
||||
public const CONTATO_TIPO = [
|
||||
'CLIENTE' => 'Cliente',
|
||||
'FORNECEDOR' => 'Fornecedor'
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::CONTATO_TIPO;
|
||||
}
|
||||
|
||||
public static function isValid(string $value): bool {
|
||||
return array_key_exists(key: $value, array: self::CONTATO_TIPO);
|
||||
}
|
||||
|
||||
public static function getDescription(string $key): ?string {
|
||||
return self::CONTATO_TIPO[$key] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Controllers;
|
||||
|
||||
use KrothiumAPI\Utils\HttpUtil;
|
||||
use WorkbloomERP\Utils\SanitizeUtil;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
|
||||
use WorkbloomERP\Module\v0\Contato\Factories\ContatoServiceFactory;
|
||||
|
||||
class ContatoController {
|
||||
public function createOptions(): void {
|
||||
try {
|
||||
$contatoService = ContatoServiceFactory::makeContatoService();
|
||||
$options = $contatoService->createOptions();
|
||||
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: 200,
|
||||
message: 'Opções para criação de contato.',
|
||||
output: ['options' => $options]
|
||||
);
|
||||
} catch (AppException $e) {
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: $e->getCode() ?? 500,
|
||||
message: $e->getMessage() ?? 'Ocorreu um erro ao obter as opções de contato.',
|
||||
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Endpoint de API para a criação de um novo **Contato**.
|
||||
*
|
||||
* Este controlador atua como o ponto de entrada para o registro de novos contatos
|
||||
* (parceiros de negócio, fornecedores ou clientes) no sistema. O método é
|
||||
* responsável por extrair os dados da requisição HTTP (POST), aplicar a sanitização
|
||||
* necessária para garantir a integridade dos dados e delegar a criação à camada de
|
||||
* serviço utilizando um `ContatoCreateDTO`.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Captura e Validação:** Obtém o corpo da requisição (JSON/Form). Valida a presença de dados, retornando erro 422 caso esteja vazio.
|
||||
* 2. **Normalização (DTO):** Instancia um `ContatoCreateDTO` populando-o com os dados sanitizados através de `SanitizeUtil`.
|
||||
* 3. **Orquestração de Serviço:** Invoca `ContatoService::create()` para validar regras de negócio, persistência e possíveis integrações.
|
||||
* 4. **Resposta:** Retorna um JSON estruturado com o status da operação (201 para sucesso ou status de erro correspondente).
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - O controlador segue o padrão de "Thin Controller", garantindo que as regras de validação complexas e a persistência de dados ocorram na camada de serviço.
|
||||
* - Todos os campos de entrada são submetidos a métodos de `SanitizeUtil` para prevenir injeções e garantir o tipo esperado antes de atingir o DTO.
|
||||
* - Utiliza `ContatoServiceFactory` para a injeção de dependência do serviço.
|
||||
*
|
||||
* @return void O método encerra o processamento enviando uma resposta HTTP JSON ao cliente.
|
||||
* @see ContatoCreateDTO Para a estrutura de dados esperada para a criação.
|
||||
* @see ContatoService::create() Para a lógica de negócio de persistência do contato.
|
||||
*/
|
||||
public function create(): void {
|
||||
try {
|
||||
$form = HttpUtil::getRequestBody(form_type: 'POST');
|
||||
if (empty($form)) {
|
||||
throw new AppException(message: 'Requisição inválida.', code: 422);
|
||||
}
|
||||
|
||||
$contaoService = ContatoServiceFactory::makeContatoService();
|
||||
$response = $contaoService->create(
|
||||
contatoCreateDTO: new ContatoCreateDTO(
|
||||
is_active: SanitizeUtil::boolean(value: $form['is_active'] ?? null),
|
||||
tipo: SanitizeUtil::string(value: $form['tipo'] ?? null),
|
||||
nome_empresarial: SanitizeUtil::string(value: $form['nome_empresarial'] ?? null),
|
||||
nome_fantasia: SanitizeUtil::string(value: $form['nome_fantasia'] ?? null),
|
||||
personalidade: SanitizeUtil::string(value: $form['personalidade'] ?? null),
|
||||
document_cpf: SanitizeUtil::string(value: $form['document_cpf'] ?? null),
|
||||
document_cnpj: SanitizeUtil::string(value: $form['document_cnpj'] ?? null),
|
||||
regime_tributario: SanitizeUtil::int(value: $form['regime_tributario'] ?? null),
|
||||
contribuinte_icms: SanitizeUtil::int(value: $form['contribuinte_icms'] ?? null),
|
||||
orgao_publico: SanitizeUtil::string(value: $form['orgao_publico'] ?? null),
|
||||
document_ie: SanitizeUtil::string(value: $form['document_ie'] ?? null),
|
||||
document_im: SanitizeUtil::string(value: $form['document_im'] ?? null),
|
||||
document_is: SanitizeUtil::string(value: $form['document_is'] ?? null),
|
||||
end_cep: SanitizeUtil::string(value: $form['end_cep'] ?? null),
|
||||
end_ibge: SanitizeUtil::string(value: $form['end_ibge'] ?? null),
|
||||
end_logradouro: SanitizeUtil::string(value: $form['end_logradouro'] ?? null),
|
||||
end_numero: SanitizeUtil::string(value: $form['end_numero'] ?? null),
|
||||
end_complemento: SanitizeUtil::string(value: $form['end_complemento'] ?? null),
|
||||
end_bairro: SanitizeUtil::string(value: $form['end_bairro'] ?? null),
|
||||
end_cidade: SanitizeUtil::string(value: $form['end_cidade'] ?? null),
|
||||
end_uf: SanitizeUtil::string(value: $form['end_uf'] ?? null),
|
||||
info_email: SanitizeUtil::string(value: $form['info_email'] ?? null),
|
||||
info_email_nfe: SanitizeUtil::string(value: $form['info_email_nfe'] ?? null),
|
||||
info_observacao: SanitizeUtil::string(value: $form['info_observacao'] ?? null),
|
||||
info_telefone: SanitizeUtil::string(value: $form['info_telefone'] ?? null),
|
||||
info_uso_consumo_ibs_cbs: SanitizeUtil::string(value: $form['info_uso_consumo_ibs_cbs'] ?? null),
|
||||
)
|
||||
);
|
||||
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: $response['response_code'] ?? 201,
|
||||
message: $response['message'] ?? 'Contato criado com sucesso.',
|
||||
output: $response['output'] ?? null
|
||||
);
|
||||
} catch (AppException $e) {
|
||||
HttpUtil::jsonResponse(
|
||||
response_code: $e->getCode() ?? 500,
|
||||
message: $e->getMessage() ?? 'Ocorreu um erro ao criar o contato.',
|
||||
output: $e->getDetails() ? ['errors' => $e->getDetails()] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\DTOs;
|
||||
|
||||
class ContatoCreateDTO {
|
||||
public function __construct(
|
||||
private ?int $is_active = null,
|
||||
private ?string $tipo = null,
|
||||
private ?string $nome_empresarial = null,
|
||||
private ?string $nome_fantasia = null,
|
||||
private ?string $personalidade = null,
|
||||
private ?string $document_cpf = null,
|
||||
private ?string $document_cnpj = null,
|
||||
private ?int $regime_tributario = null,
|
||||
private ?int $contribuinte_icms = null,
|
||||
private ?string $orgao_publico = null,
|
||||
private ?string $document_ie = null,
|
||||
private ?string $document_im = null,
|
||||
private ?string $document_is = null,
|
||||
private ?string $end_cep = null,
|
||||
private ?string $end_ibge = null,
|
||||
private ?string $end_logradouro = null,
|
||||
private ?string $end_numero = null,
|
||||
private ?string $end_complemento = null,
|
||||
private ?string $end_bairro = null,
|
||||
private ?string $end_cidade = null,
|
||||
private ?string $end_uf = null,
|
||||
private ?string $info_email = null,
|
||||
private ?string $info_email_nfe = null,
|
||||
private ?string $info_observacao = null,
|
||||
private ?string $info_telefone = null,
|
||||
private ?string $info_uso_consumo_ibs_cbs = null,
|
||||
) {}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'is_active' => $this->getIsActive(),
|
||||
'tipo' => $this->getTipo(),
|
||||
'nome_empresarial' => $this->getNomeEmpresarial(),
|
||||
'nome_fantasia' => $this->getNomeFantasia(),
|
||||
'personalidade' => $this->getPersonalidade(),
|
||||
'document_cpf' => $this->getDocumentCpf(),
|
||||
'document_cnpj' => $this->getDocumentCnpj(),
|
||||
'regime_tributario' => $this->getRegimeTributario(),
|
||||
'contribuinte_icms' => $this->getContribuinteIcms(),
|
||||
'orgao_publico' => $this->getOrgaoPublico(),
|
||||
'document_ie' => $this->getDocumentIe(),
|
||||
'document_im' => $this->getDocumentIm(),
|
||||
'document_is' => $this->getDocumentIs(),
|
||||
'end_cep' => $this->getEndCep(),
|
||||
'end_ibge' => $this->getEndIbge(),
|
||||
'end_logradouro' => $this->getEndLogradouro(),
|
||||
'end_numero' => $this->getEndNumero(),
|
||||
'end_complemento' => $this->getEndComplemento(),
|
||||
'end_bairro' => $this->getEndBairro(),
|
||||
'end_cidade' => $this->getEndCidade(),
|
||||
'end_uf' => $this->getEndUf(),
|
||||
'info_email' => $this->getInfoEmail(),
|
||||
'info_email_nfe' => $this->getInfoEmailNfe(),
|
||||
'info_observacao' => $this->getInfoObservacao(),
|
||||
'info_telefone' => $this->getInfoTelefone(),
|
||||
'info_uso_consumo_ibs_cbs' => $this->getInfoUsoConsumoIbsCbs(),
|
||||
];
|
||||
}
|
||||
|
||||
public function setIsActive(?int $is_active): void {
|
||||
$this->is_active = $is_active;
|
||||
}
|
||||
public function getIsActive(): ?int {
|
||||
return $this->is_active;
|
||||
}
|
||||
|
||||
public function setTipo(?string $tipo): void {
|
||||
$this->tipo = $tipo;
|
||||
}
|
||||
public function getTipo(): ?string {
|
||||
return $this->tipo;
|
||||
}
|
||||
|
||||
public function setNomeEmpresarial(?string $nome_empresarial): void {
|
||||
$this->nome_empresarial = $nome_empresarial;
|
||||
}
|
||||
public function getNomeEmpresarial(): ?string {
|
||||
return $this->nome_empresarial;
|
||||
}
|
||||
|
||||
public function setNomeFantasia(?string $nome_fantasia): void {
|
||||
$this->nome_fantasia = $nome_fantasia;
|
||||
}
|
||||
public function getNomeFantasia(): ?string {
|
||||
return $this->nome_fantasia;
|
||||
}
|
||||
|
||||
public function setPersonalidade(?string $personalidade): void {
|
||||
$this->personalidade = $personalidade;
|
||||
}
|
||||
public function getPersonalidade(): ?string {
|
||||
return $this->personalidade;
|
||||
}
|
||||
|
||||
public function setDocumentCpf(?string $document_cpf): void {
|
||||
$this->document_cpf = $document_cpf;
|
||||
}
|
||||
public function getDocumentCpf(): ?string {
|
||||
return $this->document_cpf;
|
||||
}
|
||||
|
||||
public function setDocumentCnpj(?string $document_cnpj): void {
|
||||
$this->document_cnpj = $document_cnpj;
|
||||
}
|
||||
public function getDocumentCnpj(): ?string {
|
||||
return $this->document_cnpj;
|
||||
}
|
||||
|
||||
public function setRegimeTributario(?int $regime_tributario): void {
|
||||
$this->regime_tributario = $regime_tributario;
|
||||
}
|
||||
public function getRegimeTributario(): ?int {
|
||||
return $this->regime_tributario;
|
||||
}
|
||||
|
||||
public function setContribuinteIcms(?int $contribuinte_icms): void {
|
||||
$this->contribuinte_icms = $contribuinte_icms;
|
||||
}
|
||||
public function getContribuinteIcms(): ?int {
|
||||
return $this->contribuinte_icms;
|
||||
}
|
||||
|
||||
public function setOrgaoPublico(?string $orgao_publico): void {
|
||||
$this->orgao_publico = $orgao_publico;
|
||||
}
|
||||
public function getOrgaoPublico(): ?string {
|
||||
return $this->orgao_publico;
|
||||
}
|
||||
|
||||
public function setDocumentIe(?string $document_ie): void {
|
||||
$this->document_ie = $document_ie;
|
||||
}
|
||||
public function getDocumentIe(): ?string {
|
||||
return $this->document_ie;
|
||||
}
|
||||
|
||||
public function setDocumentIm(?string $document_im): void {
|
||||
$this->document_im = $document_im;
|
||||
}
|
||||
public function getDocumentIm(): ?string {
|
||||
return $this->document_im;
|
||||
}
|
||||
|
||||
public function setDocumentIs(?string $document_is): void {
|
||||
$this->document_is = $document_is;
|
||||
}
|
||||
public function getDocumentIs(): ?string {
|
||||
return $this->document_is;
|
||||
}
|
||||
|
||||
public function setEndCep(?string $end_cep): void {
|
||||
$this->end_cep = $end_cep;
|
||||
}
|
||||
public function getEndCep(): ?string {
|
||||
return $this->end_cep;
|
||||
}
|
||||
|
||||
public function setEndIbge(?string $end_ibge): void {
|
||||
$this->end_ibge = $end_ibge;
|
||||
}
|
||||
public function getEndIbge(): ?string {
|
||||
return $this->end_ibge;
|
||||
}
|
||||
|
||||
public function setEndLogradouro(?string $end_logradouro): void {
|
||||
$this->end_logradouro = $end_logradouro;
|
||||
}
|
||||
public function getEndLogradouro(): ?string {
|
||||
return $this->end_logradouro;
|
||||
}
|
||||
|
||||
public function setEndNumero(?string $end_numero): void {
|
||||
$this->end_numero = $end_numero;
|
||||
}
|
||||
public function getEndNumero(): ?string {
|
||||
return $this->end_numero;
|
||||
}
|
||||
|
||||
public function setEndComplemento(?string $end_complemento): void {
|
||||
$this->end_complemento = $end_complemento;
|
||||
}
|
||||
public function getEndComplemento(): ?string {
|
||||
return $this->end_complemento;
|
||||
}
|
||||
|
||||
public function setEndBairro(?string $end_bairro): void {
|
||||
$this->end_bairro = $end_bairro;
|
||||
}
|
||||
public function getEndBairro(): ?string {
|
||||
return $this->end_bairro;
|
||||
}
|
||||
|
||||
public function setEndCidade(?string $end_cidade): void {
|
||||
$this->end_cidade = $end_cidade;
|
||||
}
|
||||
public function getEndCidade(): ?string {
|
||||
return $this->end_cidade;
|
||||
}
|
||||
|
||||
public function setEndUf(?string $end_uf): void {
|
||||
$this->end_uf = $end_uf;
|
||||
}
|
||||
public function getEndUf(): ?string {
|
||||
return $this->end_uf;
|
||||
}
|
||||
|
||||
public function setInfoEmail(?string $info_email): void {
|
||||
$this->info_email = $info_email;
|
||||
}
|
||||
public function getInfoEmail(): ?string {
|
||||
return $this->info_email;
|
||||
}
|
||||
|
||||
public function setInfoEmailNfe(?string $info_email_nfe): void {
|
||||
$this->info_email_nfe = $info_email_nfe;
|
||||
}
|
||||
public function getInfoEmailNfe(): ?string {
|
||||
return $this->info_email_nfe;
|
||||
}
|
||||
|
||||
public function setInfoObservacao(?string $info_observacao): void {
|
||||
$this->info_observacao = $info_observacao;
|
||||
}
|
||||
public function getInfoObservacao(): ?string {
|
||||
return $this->info_observacao;
|
||||
}
|
||||
|
||||
public function setInfoTelefone(?string $info_telefone): void {
|
||||
$this->info_telefone = $info_telefone;
|
||||
}
|
||||
public function getInfoTelefone(): ?string {
|
||||
return $this->info_telefone;
|
||||
}
|
||||
|
||||
public function setInfoUsoConsumoIbsCbs(?string $info_uso_consumo_ibs_cbs): void {
|
||||
$this->info_uso_consumo_ibs_cbs = $info_uso_consumo_ibs_cbs;
|
||||
}
|
||||
public function getInfoUsoConsumoIbsCbs(): ?string {
|
||||
return $this->info_uso_consumo_ibs_cbs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Factories;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Utils\ValidateUtil;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Contato\Repos\ContatoRepo;
|
||||
use WorkbloomERP\Module\v0\Contato\Models\ContatoModel;
|
||||
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
|
||||
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
|
||||
|
||||
// Constantes para opções de contato
|
||||
use WorkbloomERP\Constants\SpedCRTConst;
|
||||
use WorkbloomERP\Constants\BrasilUfsConst;
|
||||
use WorkbloomERP\Constants\SpedPaisesConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\OrgaoPublicoConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\PersonalidadeConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\ContribuinteICMSConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\NFSeConsumoIbsCbsConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\TipoConst;
|
||||
|
||||
class ContatoFactory {
|
||||
public function __construct(private EmpresaModel $empresaModel, private ContatoCreateDTO $contatoCreateDTO, private ContatoRepo $contatoRepo, private EmpresaRepo $empresaRepo, private DBService $db) {
|
||||
if (!$empresaModel) {
|
||||
throw new AppException(message: 'Empresa não encontrada.', code: 404);
|
||||
}
|
||||
if (!$contatoCreateDTO) {
|
||||
throw new AppException(message: 'Dados do contato inválidos.', code: 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function create(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): ?ContatoModel {
|
||||
try {
|
||||
// Valida os campos comuns a ambos os tipos de contato (PF e PJ)
|
||||
$this->validateCommonFields(contatoCreateDTO: $contatoCreateDTO);
|
||||
|
||||
// Cria o contato com base na personalidade (PF ou PJ)
|
||||
$contatoModel = match($contatoCreateDTO->getPersonalidade()) {
|
||||
'PF' => $this->createPF(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel),
|
||||
'PJ' => $this->createPJ(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel)
|
||||
};
|
||||
|
||||
return $contatoModel;
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateCommonFields(ContatoCreateDTO $contatoCreateDTO): void {
|
||||
$errors = [];
|
||||
|
||||
$commonFields = [
|
||||
'is_active',
|
||||
'tipo',
|
||||
'nome_empresarial',
|
||||
'personalidade',
|
||||
'regime_tributario',
|
||||
'contribuinte_icms',
|
||||
'orgao_publico',
|
||||
'end_cep',
|
||||
'end_logradouro',
|
||||
'end_numero',
|
||||
'end_bairro',
|
||||
'end_cidade',
|
||||
'end_uf',
|
||||
'info_email',
|
||||
'info_email_nfe',
|
||||
'info_telefone',
|
||||
'info_uso_consumo_ibs_cbs',
|
||||
];
|
||||
|
||||
foreach ($commonFields as $field) {
|
||||
if (!array_key_exists(key: $field, array: $contatoCreateDTO->toArray()) || $contatoCreateDTO->toArray()[$field] === '') {
|
||||
$errors[$field] = 'Campo obrigatório.';
|
||||
}
|
||||
}
|
||||
|
||||
// Verifica se o campo 'tipo' tem um valor válido
|
||||
$tipo = $contatoCreateDTO->getTipo() ? strtoupper(string: $contatoCreateDTO->getTipo()) : null;
|
||||
if ($tipo && !TipoConst::isValid(value: $tipo)) {
|
||||
$errors['tipo'] = "Valor inválido.";
|
||||
}
|
||||
|
||||
// Verifica se o campo 'personalidade' tem um valor válido
|
||||
$personalidade = $contatoCreateDTO->getPersonalidade() ? strtoupper(string: $contatoCreateDTO->getPersonalidade()) : null;
|
||||
if ($personalidade && !PersonalidadeConst::isValid(value: $personalidade)) {
|
||||
$errors['personalidade'] = "Valor inválido.";
|
||||
}
|
||||
|
||||
// Valida o regime tributário
|
||||
if (!in_array($contatoCreateDTO->getRegimeTributario(), [1, 2, 3, 4, null], true)) {
|
||||
$errors['regime_tributario'] = "Valor inválido.";
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
throw new AppException(message: 'Dados de contato inválidos.',code: 422, details: $errors);
|
||||
}
|
||||
}
|
||||
|
||||
private function createPF(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): ContatoModel {
|
||||
try {
|
||||
// Valida os campos específicos para pessoa física
|
||||
$this->validatePFFields(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel);
|
||||
|
||||
$contatoModel = new ContatoModel();
|
||||
$contatoModel->setUuid(Uuid::uuid7()->toString());
|
||||
$contatoModel->setEmpresaId($empresaModel->getId());
|
||||
$contatoModel->setIsActive($contatoCreateDTO->getIsActive());
|
||||
$contatoModel->setTipo($contatoCreateDTO->getTipo());
|
||||
$contatoModel->setNomeEmpresarial($contatoCreateDTO->getNomeEmpresarial());
|
||||
$contatoModel->setPersonalidade($contatoCreateDTO->getPersonalidade());
|
||||
$contatoModel->setRegimeTributario($contatoCreateDTO->getRegimeTributario());
|
||||
$contatoModel->setContribuinteIcms($contatoCreateDTO->getContribuinteIcms());
|
||||
$contatoModel->setOrgaoPublico($contatoCreateDTO->getOrgaoPublico());
|
||||
$contatoModel->setEndCep($contatoCreateDTO->getEndCep());
|
||||
$contatoModel->setEndIbge($contatoCreateDTO->getEndIbge());
|
||||
$contatoModel->setEndLogradouro($contatoCreateDTO->getEndLogradouro());
|
||||
$contatoModel->setEndNumero($contatoCreateDTO->getEndNumero());
|
||||
$contatoModel->setEndBairro($contatoCreateDTO->getEndBairro());
|
||||
$contatoModel->setEndCidade($contatoCreateDTO->getEndCidade());
|
||||
$contatoModel->setEndUf($contatoCreateDTO->getEndUf());
|
||||
$contatoModel->setInfoEmail($contatoCreateDTO->getInfoEmail());
|
||||
$contatoModel->setInfoEmailNfe($contatoCreateDTO->getInfoEmailNfe());
|
||||
$contatoModel->setInfoObservacao($contatoCreateDTO->getInfoObservacao());
|
||||
$contatoModel->setInfoTelefone($contatoCreateDTO->getInfoTelefone());
|
||||
$contatoModel->setInfoUsoConsumoIbsCbs($contatoCreateDTO->getInfoUsoConsumoIbsCbs());
|
||||
$contatoModel->setCreatedAt(new DateTimeImmutable());
|
||||
|
||||
return $contatoModel;
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function validatePFFields(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): void {
|
||||
$errors = [];
|
||||
|
||||
if (($contatoCreateDTO->getDocumentCpf() === '') || !ValidateUtil::cpf(cpf: $contatoCreateDTO->getDocumentCpf())) {
|
||||
$errors['document_cpf'] = 'CPF informado é inválido.';
|
||||
throw new AppException(message: 'Dados de contato inválidos.', code: 422, details: $errors);
|
||||
}
|
||||
|
||||
$existingContato = $this->contatoRepo->findOneByConditions(
|
||||
empresa_id: $empresaModel->getId(),
|
||||
conditions: [
|
||||
['field' => 'document_cpf', 'operator' => '=', 'value' => $contatoCreateDTO->getDocumentCpf()]
|
||||
]
|
||||
);
|
||||
|
||||
if ($existingContato) {
|
||||
$errors['document_cpf'] = 'CPF já cadastrado para esta empresa.';
|
||||
throw new AppException(message: 'Dados de contato inválidos.', code: 422, details: $errors);
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
throw new AppException(message: 'Dados de contato inválidos.', code: 422, details: $errors);
|
||||
}
|
||||
}
|
||||
|
||||
private function createPJ(ContatoCreateDTO $contatoCreateDTO, EmpresaModel $empresaModel): ContatoModel {
|
||||
try {
|
||||
// Valida os campos específicos para pessoa física
|
||||
// $this->validatePFFields(contatoCreateDTO: $contatoCreateDTO);
|
||||
|
||||
return new ContatoModel();
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Factories;
|
||||
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Contato\Repos\ContatoRepo;
|
||||
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
|
||||
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
|
||||
use WorkbloomERP\Module\v0\Contato\Services\ContatoService;
|
||||
|
||||
class ContatoServiceFactory {
|
||||
public static function makeContatoService(): ContatoService {
|
||||
$db = new DBService();
|
||||
return new ContatoService(
|
||||
db: $db,
|
||||
contatoRepo: new ContatoRepo(db: $db),
|
||||
empresaRepo: new EmpresaRepo(db: $db),
|
||||
);
|
||||
}
|
||||
|
||||
public static function makeContratoFactory(EmpresaModel $empresaModel, ContatoCreateDTO $contatoCreateDTO): ContatoFactory {
|
||||
$db = new DBService();
|
||||
return new ContatoFactory(
|
||||
db: $db,
|
||||
empresaModel: $empresaModel,
|
||||
contatoCreateDTO: $contatoCreateDTO,
|
||||
contatoRepo: new ContatoRepo(db: $db),
|
||||
empresaRepo: new EmpresaRepo(db: $db),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Models;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
|
||||
class ContatoModel {
|
||||
public function __construct(
|
||||
private ?int $id = null,
|
||||
private ?string $uuid = null,
|
||||
private ?int $empresa_id = null,
|
||||
private ?bool $is_active = null,
|
||||
private ?string $tipo = null,
|
||||
private ?string $nome_empresarial = null,
|
||||
private ?string $nome_fantasia = null,
|
||||
private ?string $personalidade = null,
|
||||
private ?string $document_cpf = null,
|
||||
private ?string $document_cnpj = null,
|
||||
private ?int $regime_tributario = null,
|
||||
private ?int $contribuinte_icms = null,
|
||||
private ?string $orgao_publico = null,
|
||||
private ?string $document_ie = null,
|
||||
private ?string $document_im = null,
|
||||
private ?string $document_is = null,
|
||||
private ?string $end_pais = null,
|
||||
private ?string $end_cep = null,
|
||||
private ?string $end_ibge = null,
|
||||
private ?string $end_logradouro = null,
|
||||
private ?string $end_numero = null,
|
||||
private ?string $end_complemento = null,
|
||||
private ?string $end_bairro = null,
|
||||
private ?string $end_cidade = null,
|
||||
private ?string $end_uf = null,
|
||||
private ?string $info_email = null,
|
||||
private ?string $info_email_nfe = null,
|
||||
private ?string $info_observacao = null,
|
||||
private ?string $info_telefone = null,
|
||||
private ?int $info_uso_consumo_ibs_cbs = null,
|
||||
private ?DateTimeImmutable $created_at = null,
|
||||
private ?DateTimeImmutable $updated_at = null,
|
||||
private ?DateTimeImmutable $deleted_at = null,
|
||||
) {}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'uuid' => $this->getUuid(),
|
||||
'empresa_id' => $this->getEmpresaId(),
|
||||
'is_active' => $this->getIsActive(),
|
||||
'tipo' => $this->getTipo(),
|
||||
'nome_empresarial' => $this->getNomeEmpresarial(),
|
||||
'nome_fantasia' => $this->getNomeFantasia(),
|
||||
'personalidade' => $this->getPersonalidade(),
|
||||
'document_cpf' => $this->getDocumentCpf(),
|
||||
'document_cnpj' => $this->getDocumentCnpj(),
|
||||
'regime_tributario' => $this->getRegimeTributario(),
|
||||
'contribuinte_icms' => $this->getContribuinteIcms(),
|
||||
'orgao_publico' => $this->getOrgaoPublico(),
|
||||
'document_ie' => $this->getDocumentIe(),
|
||||
'document_im' => $this->getDocumentIm(),
|
||||
'document_is' => $this->getDocumentIs(),
|
||||
'end_pais' => $this->getEndPais(),
|
||||
'end_cep' => $this->getEndCep(),
|
||||
'end_ibge' => $this->getEndIbge(),
|
||||
'end_logradouro' => $this->getEndLogradouro(),
|
||||
'end_numero' => $this->getEndNumero(),
|
||||
'end_complemento' => $this->getEndComplemento(),
|
||||
'end_bairro' => $this->getEndBairro(),
|
||||
'end_cidade' => $this->getEndCidade(),
|
||||
'end_uf' => $this->getEndUf(),
|
||||
'info_email' => $this->getInfoEmail(),
|
||||
'info_email_nfe' => $this->getInfoEmailNfe(),
|
||||
'info_observacao' => $this->getInfoObservacao(),
|
||||
'info_telefone' => $this->getInfoTelefone(),
|
||||
'info_uso_consumo_ibs_cbs' => $this->getInfoUsoConsumoIbsCbs(),
|
||||
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format(format: 'Y-m-d H:i:s') : null,
|
||||
'updated_at' => $this->getUpdatedAt() ? $this->getUpdatedAt()->format(format: 'Y-m-d H:i:s') : null,
|
||||
'deleted_at' => $this->getDeletedAt() ? $this->getDeletedAt()->format(format: 'Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function setId(?int $id): void {
|
||||
$this->id = $id;
|
||||
}
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setUuid(?string $uuid): void {
|
||||
$this->uuid = $uuid;
|
||||
}
|
||||
public function getUuid(): ?string {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function setEmpresaId(?int $empresa_id): void {
|
||||
$this->empresa_id = $empresa_id;
|
||||
}
|
||||
public function getEmpresaId(): ?int {
|
||||
return $this->empresa_id;
|
||||
}
|
||||
|
||||
public function setIsActive(?bool $is_active): void {
|
||||
$this->is_active = $is_active;
|
||||
}
|
||||
public function getIsActive(): ?bool {
|
||||
return $this->is_active;
|
||||
}
|
||||
|
||||
public function setTipo(?string $tipo): void {
|
||||
$this->tipo = $tipo;
|
||||
}
|
||||
public function getTipo(): ?string {
|
||||
return $this->tipo;
|
||||
}
|
||||
|
||||
public function setNomeEmpresarial(?string $nome_empresarial): void {
|
||||
$this->nome_empresarial = $nome_empresarial;
|
||||
}
|
||||
public function getNomeEmpresarial(): ?string {
|
||||
return $this->nome_empresarial;
|
||||
}
|
||||
|
||||
public function setNomeFantasia(?string $nome_fantasia): void {
|
||||
$this->nome_fantasia = $nome_fantasia;
|
||||
}
|
||||
public function getNomeFantasia(): ?string {
|
||||
return $this->nome_fantasia;
|
||||
}
|
||||
|
||||
public function setPersonalidade(?string $personalidade): void {
|
||||
$this->personalidade = $personalidade;
|
||||
}
|
||||
public function getPersonalidade(): ?string {
|
||||
return $this->personalidade;
|
||||
}
|
||||
|
||||
public function setDocumentCpf(?string $document_cpf): void {
|
||||
$this->document_cpf = $document_cpf;
|
||||
}
|
||||
public function getDocumentCpf(): ?string {
|
||||
return $this->document_cpf;
|
||||
}
|
||||
|
||||
public function setDocumentCnpj(?string $document_cnpj): void {
|
||||
$this->document_cnpj = $document_cnpj;
|
||||
}
|
||||
public function getDocumentCnpj(): ?string {
|
||||
return $this->document_cnpj;
|
||||
}
|
||||
|
||||
public function setRegimeTributario(?int $regime_tributario): void {
|
||||
$this->regime_tributario = $regime_tributario;
|
||||
}
|
||||
public function getRegimeTributario(): ?int {
|
||||
return $this->regime_tributario;
|
||||
}
|
||||
|
||||
public function setContribuinteIcms(?int $contribuinte_icms): void {
|
||||
$this->contribuinte_icms = $contribuinte_icms;
|
||||
}
|
||||
public function getContribuinteIcms(): ?int {
|
||||
return $this->contribuinte_icms;
|
||||
}
|
||||
|
||||
public function setOrgaoPublico(?string $orgao_publico): void {
|
||||
$this->orgao_publico = $orgao_publico;
|
||||
}
|
||||
public function getOrgaoPublico(): ?string {
|
||||
return $this->orgao_publico;
|
||||
}
|
||||
|
||||
public function setDocumentIe(?string $document_ie): void {
|
||||
$this->document_ie = $document_ie;
|
||||
}
|
||||
public function getDocumentIe(): ?string {
|
||||
return $this->document_ie;
|
||||
}
|
||||
|
||||
public function setDocumentIm(?string $document_im): void {
|
||||
$this->document_im = $document_im;
|
||||
}
|
||||
public function getDocumentIm(): ?string {
|
||||
return $this->document_im;
|
||||
}
|
||||
|
||||
public function setDocumentIs(?string $document_is): void {
|
||||
$this->document_is = $document_is;
|
||||
}
|
||||
public function getDocumentIs(): ?string {
|
||||
return $this->document_is;
|
||||
}
|
||||
|
||||
public function setEndPais(?string $end_pais): void {
|
||||
$this->end_pais = $end_pais;
|
||||
}
|
||||
public function getEndPais(): ?string {
|
||||
return $this->end_pais;
|
||||
}
|
||||
|
||||
public function setEndCep(?string $end_cep): void {
|
||||
$this->end_cep = $end_cep;
|
||||
}
|
||||
public function getEndCep(): ?string {
|
||||
return $this->end_cep;
|
||||
}
|
||||
|
||||
public function setEndIbge(?string $end_ibge): void {
|
||||
$this->end_ibge = $end_ibge;
|
||||
}
|
||||
public function getEndIbge(): ?string {
|
||||
return $this->end_ibge;
|
||||
}
|
||||
|
||||
public function setEndLogradouro(?string $end_logradouro): void {
|
||||
$this->end_logradouro = $end_logradouro;
|
||||
}
|
||||
public function getEndLogradouro(): ?string {
|
||||
return $this->end_logradouro;
|
||||
}
|
||||
|
||||
public function setEndNumero(?string $end_numero): void {
|
||||
$this->end_numero = $end_numero;
|
||||
}
|
||||
public function getEndNumero(): ?string {
|
||||
return $this->end_numero;
|
||||
}
|
||||
|
||||
public function setEndComplemento(?string $end_complemento): void {
|
||||
$this->end_complemento = $end_complemento;
|
||||
}
|
||||
public function getEndComplemento(): ?string {
|
||||
return $this->end_complemento;
|
||||
}
|
||||
|
||||
public function setEndBairro(?string $end_bairro): void {
|
||||
$this->end_bairro = $end_bairro;
|
||||
}
|
||||
public function getEndBairro(): ?string {
|
||||
return $this->end_bairro;
|
||||
}
|
||||
|
||||
public function setEndCidade(?string $end_cidade): void {
|
||||
$this->end_cidade = $end_cidade;
|
||||
}
|
||||
public function getEndCidade(): ?string {
|
||||
return $this->end_cidade;
|
||||
}
|
||||
|
||||
public function setEndUf(?string $end_uf): void {
|
||||
$this->end_uf = $end_uf;
|
||||
}
|
||||
public function getEndUf(): ?string {
|
||||
return $this->end_uf;
|
||||
}
|
||||
|
||||
public function setInfoEmail(?string $info_email): void {
|
||||
$this->info_email = $info_email;
|
||||
}
|
||||
public function getInfoEmail(): ?string {
|
||||
return $this->info_email;
|
||||
}
|
||||
|
||||
public function setInfoEmailNfe(?string $info_email_nfe): void {
|
||||
$this->info_email_nfe = $info_email_nfe;
|
||||
}
|
||||
public function getInfoEmailNfe(): ?string {
|
||||
return $this->info_email_nfe;
|
||||
}
|
||||
|
||||
public function setInfoObservacao(?string $info_observacao): void {
|
||||
$this->info_observacao = $info_observacao;
|
||||
}
|
||||
public function getInfoObservacao(): ?string {
|
||||
return $this->info_observacao;
|
||||
}
|
||||
|
||||
public function setInfoTelefone(?string $info_telefone): void {
|
||||
$this->info_telefone = $info_telefone;
|
||||
}
|
||||
public function getInfoTelefone(): ?string {
|
||||
return $this->info_telefone;
|
||||
}
|
||||
|
||||
public function setInfoUsoConsumoIbsCbs(?int $info_uso_consumo_ibs_cbs): void {
|
||||
$this->info_uso_consumo_ibs_cbs = $info_uso_consumo_ibs_cbs;
|
||||
}
|
||||
public function getInfoUsoConsumoIbsCbs(): ?int {
|
||||
return $this->info_uso_consumo_ibs_cbs;
|
||||
}
|
||||
|
||||
public function setCreatedAt(?DateTimeImmutable $created_at): void {
|
||||
$this->created_at = $created_at;
|
||||
}
|
||||
public function getCreatedAt(): ?DateTimeImmutable {
|
||||
return $this->created_at;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(?DateTimeImmutable $updated_at): void {
|
||||
$this->updated_at = $updated_at;
|
||||
}
|
||||
public function getUpdatedAt(): ?DateTimeImmutable {
|
||||
return $this->updated_at;
|
||||
}
|
||||
|
||||
public function setDeletedAt(?DateTimeImmutable $deleted_at): void {
|
||||
$this->deleted_at = $deleted_at;
|
||||
}
|
||||
public function getDeletedAt(): ?DateTimeImmutable {
|
||||
return $this->deleted_at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Repos;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Module\v0\Contato\Models\ContatoModel;
|
||||
|
||||
class ContatoRepo {
|
||||
protected string $contatoTable = 'contato';
|
||||
public function __construct(
|
||||
private DBService $db
|
||||
) {}
|
||||
|
||||
public function insert(ContatoModel $contatoModel): ContatoModel {
|
||||
$query =
|
||||
"INSERT INTO {$this->contatoTable} (
|
||||
uuid,
|
||||
empresa_id,
|
||||
is_active,
|
||||
tipo,
|
||||
nome_empresarial,
|
||||
nome_fantasia,
|
||||
personalidade,
|
||||
document_cpf,
|
||||
document_cnpj,
|
||||
regime_tributario,
|
||||
contribuinte_icms,
|
||||
orgao_publico,
|
||||
document_ie,
|
||||
document_im,
|
||||
document_is,
|
||||
end_pais,
|
||||
end_cep,
|
||||
end_ibge,
|
||||
end_logradouro,
|
||||
end_numero,
|
||||
end_complemento,
|
||||
end_bairro,
|
||||
end_cidade,
|
||||
end_uf,
|
||||
info_email,
|
||||
info_email_nfe,
|
||||
info_observacao,
|
||||
info_telefone,
|
||||
info_uso_consumo_ibs_cbs,
|
||||
created_at
|
||||
) VALUES (
|
||||
:uuid,
|
||||
:empresa_id,
|
||||
:is_active,
|
||||
:tipo,
|
||||
:nome_empresarial,
|
||||
:nome_fantasia,
|
||||
:personalidade,
|
||||
:document_cpf,
|
||||
:document_cnpj,
|
||||
:regime_tributario,
|
||||
:contribuinte_icms,
|
||||
:orgao_publico,
|
||||
:document_ie,
|
||||
:document_im,
|
||||
:document_is,
|
||||
:end_pais,
|
||||
:end_cep,
|
||||
:end_ibge,
|
||||
:end_logradouro,
|
||||
:end_numero,
|
||||
:end_complemento,
|
||||
:end_bairro,
|
||||
:end_cidade,
|
||||
:end_uf,
|
||||
:info_email,
|
||||
:info_email_nfe,
|
||||
:info_observacao,
|
||||
:info_telefone,
|
||||
:info_uso_consumo_ibs_cbs,
|
||||
:created_at
|
||||
)";
|
||||
|
||||
$contatoModel->setCreatedAt(new DateTimeImmutable());
|
||||
|
||||
$this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':uuid' => $contatoModel->getUuid(),
|
||||
':empresa_id' => $contatoModel->getEmpresaId(),
|
||||
':is_active' => $contatoModel->getIsActive(),
|
||||
':tipo' => $contatoModel->getTipo(),
|
||||
':nome_empresarial' => $contatoModel->getNomeEmpresarial(),
|
||||
':nome_fantasia' => $contatoModel->getNomeFantasia(),
|
||||
':personalidade' => $contatoModel->getPersonalidade(),
|
||||
':document_cpf' => $contatoModel->getDocumentCpf(),
|
||||
':document_cnpj' => $contatoModel->getDocumentCnpj(),
|
||||
':regime_tributario' => $contatoModel->getRegimeTributario(),
|
||||
':contribuinte_icms' => $contatoModel->getContribuinteIcms(),
|
||||
':orgao_publico' => $contatoModel->getOrgaoPublico(),
|
||||
':document_ie' => $contatoModel->getDocumentIe(),
|
||||
':document_im' => $contatoModel->getDocumentIm(),
|
||||
':document_is' => $contatoModel->getDocumentIs(),
|
||||
':end_pais' => $contatoModel->getEndPais(),
|
||||
':end_cep' => $contatoModel->getEndCep(),
|
||||
':end_ibge' => $contatoModel->getEndIbge(),
|
||||
':end_logradouro' => $contatoModel->getEndLogradouro(),
|
||||
':end_numero' => $contatoModel->getEndNumero(),
|
||||
':end_complemento' => $contatoModel->getEndComplemento(),
|
||||
':end_bairro' => $contatoModel->getEndBairro(),
|
||||
':end_cidade' => $contatoModel->getEndCidade(),
|
||||
':end_uf' => $contatoModel->getEndUf(),
|
||||
':info_email' => $contatoModel->getInfoEmail(),
|
||||
':info_email_nfe' => $contatoModel->getInfoEmailNfe(),
|
||||
':info_observacao' => $contatoModel->getInfoObservacao(),
|
||||
':info_telefone' => $contatoModel->getInfoTelefone(),
|
||||
':info_uso_consumo_ibs_cbs' => $contatoModel->getInfoUsoConsumoIbsCbs(),
|
||||
':created_at' => $contatoModel->getCreatedAt()->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
|
||||
$contatoModel->setId(id: $this->db->lastInsertId());
|
||||
return $contatoModel;
|
||||
}
|
||||
|
||||
public function update(ContatoModel $contatoModel): bool {
|
||||
$query =
|
||||
"UPDATE {$this->contatoTable} SET
|
||||
is_active = :is_active,
|
||||
tipo = :tipo,
|
||||
nome_empresarial = :nome_empresarial,
|
||||
nome_fantasia = :nome_fantasia,
|
||||
personalidade = :personalidade,
|
||||
document_cpf = :document_cpf,
|
||||
document_cnpj = :document_cnpj,
|
||||
regime_tributario = :regime_tributario,
|
||||
contribuinte_icms = :contribuinte_icms,
|
||||
orgao_publico = :orgao_publico,
|
||||
document_ie = :document_ie,
|
||||
document_im = :document_im,
|
||||
document_is = :document_is,
|
||||
end_pais = :end_pais,
|
||||
end_cep = :end_cep,
|
||||
end_ibge = :end_ibge,
|
||||
end_logradouro = :end_logradouro,
|
||||
end_numero = :end_numero,
|
||||
end_complemento = :end_complemento,
|
||||
end_bairro = :end_bairro,
|
||||
end_cidade = :end_cidade,
|
||||
end_uf = :end_uf,
|
||||
info_email = :info_email,
|
||||
info_email_nfe = :info_email_nfe,
|
||||
info_observacao = :info_observacao,
|
||||
info_telefone = :info_telefone,
|
||||
info_uso_consumo_ibs_cbs = :info_uso_consumo_ibs_cbs,
|
||||
updated_at = :updated_at
|
||||
WHERE id = :id OR uuid = :uuid";
|
||||
|
||||
return $this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':id' => $contatoModel->getId(),
|
||||
':uuid' => $contatoModel->getUuid(),
|
||||
':empresa_id' => $contatoModel->getEmpresaId(),
|
||||
':is_active' => $contatoModel->getIsActive(),
|
||||
':tipo' => $contatoModel->getTipo(),
|
||||
':nome_empresarial' => $contatoModel->getNomeEmpresarial(),
|
||||
':nome_fantasia' => $contatoModel->getNomeFantasia(),
|
||||
':personalidade' => $contatoModel->getPersonalidade(),
|
||||
':document_cpf' => $contatoModel->getDocumentCpf(),
|
||||
':document_cnpj' => $contatoModel->getDocumentCnpj(),
|
||||
':regime_tributario' => $contatoModel->getRegimeTributario(),
|
||||
':contribuinte_icms' => $contatoModel->getContribuinteIcms(),
|
||||
':orgao_publico' => $contatoModel->getOrgaoPublico(),
|
||||
':document_ie' => $contatoModel->getDocumentIe(),
|
||||
':document_im' => $contatoModel->getDocumentIm(),
|
||||
':document_is' => $contatoModel->getDocumentIs(),
|
||||
':end_pais' => $contatoModel->getEndPais(),
|
||||
':end_cep' => $contatoModel->getEndCep(),
|
||||
':end_ibge' => $contatoModel->getEndIbge(),
|
||||
':end_logradouro' => $contatoModel->getEndLogradouro(),
|
||||
':end_numero' => $contatoModel->getEndNumero(),
|
||||
':end_complemento' => $contatoModel->getEndComplemento(),
|
||||
':end_bairro' => $contatoModel->getEndBairro(),
|
||||
':end_cidade' => $contatoModel->getEndCidade(),
|
||||
':end_uf' => $contatoModel->getEndUf(),
|
||||
':info_email' => $contatoModel->getInfoEmail(),
|
||||
':info_email_nfe' => $contatoModel->getInfoEmailNfe(),
|
||||
':info_observacao' => $contatoModel->getInfoObservacao(),
|
||||
':info_telefone' => $contatoModel->getInfoTelefone(),
|
||||
':info_uso_consumo_ibs_cbs' => $contatoModel->getInfoUsoConsumoIbsCbs(),
|
||||
':updated_at' => $contatoModel->getUpdatedAt()->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(ContatoModel $contatoModel): bool {
|
||||
$query =
|
||||
"UPDATE {$this->contatoTable} SET
|
||||
deleted_at = :deleted_at
|
||||
WHERE id = :id OR uuid = :uuid";
|
||||
|
||||
return $this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':id' => $contatoModel->getId(),
|
||||
':uuid' => $contatoModel->getUuid(),
|
||||
':deleted_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function findByIdentifier(int $empresa_id, string $identifier, mixed $value) {
|
||||
$query =
|
||||
"SELECT
|
||||
id,
|
||||
uuid,
|
||||
empresa_id,
|
||||
is_active,
|
||||
nome_empresarial,
|
||||
nome_fantasia,
|
||||
personalidade,
|
||||
document_cpf,
|
||||
document_cnpj,
|
||||
regime_tributario,
|
||||
contribuinte_icms,
|
||||
orgao_publico,
|
||||
document_ie,
|
||||
document_im,
|
||||
document_is,
|
||||
end_cep,
|
||||
end_ibge,
|
||||
end_logradouro,
|
||||
end_numero,
|
||||
end_complemento,
|
||||
end_bairro,
|
||||
end_cidade,
|
||||
end_uf,
|
||||
info_email,
|
||||
info_email_nfe,
|
||||
info_observacao,
|
||||
info_telefone,
|
||||
info_uso_consumo_ibs_cbs,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM {$this->contatoTable}
|
||||
WHERE empresa_id = :empresa_id
|
||||
AND {$identifier} = :value
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->fetchOne(
|
||||
sql: $query,
|
||||
params: [
|
||||
':empresa_id' => $empresa_id,
|
||||
':value' => $value,
|
||||
]
|
||||
);
|
||||
|
||||
return $result ? $this->mapToModel($result) : null;
|
||||
}
|
||||
|
||||
public function findOneByConditions(int $empresa_id, array $conditions): ?ContatoModel {
|
||||
if (empty($conditions)) {
|
||||
throw new \InvalidArgumentException('O array de condições não pode estar vazio.');
|
||||
}
|
||||
|
||||
// Começa com as condições fixas
|
||||
$whereClauses = [
|
||||
'empresa_id = :empresa_id',
|
||||
'deleted_at IS NULL'
|
||||
];
|
||||
|
||||
$params = [
|
||||
':empresa_id' => $empresa_id
|
||||
];
|
||||
|
||||
// Adiciona as condições dinâmicas
|
||||
foreach ($conditions as $condition) {
|
||||
if (!isset($condition['field'], $condition['value'])) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Cada condição deve conter 'field' e 'value'."
|
||||
);
|
||||
}
|
||||
|
||||
$field = $condition['field'];
|
||||
$operator = $condition['operator'] ?? '=';
|
||||
$value = $condition['value'];
|
||||
|
||||
$whereClauses[] = "{$field} {$operator} :{$field}";
|
||||
$params[":{$field}"] = $value;
|
||||
}
|
||||
|
||||
$whereSql = implode(' AND ', $whereClauses);
|
||||
|
||||
$query =
|
||||
"SELECT
|
||||
id,
|
||||
uuid,
|
||||
empresa_id,
|
||||
is_active,
|
||||
tipo,
|
||||
nome_empresarial,
|
||||
nome_fantasia,
|
||||
personalidade,
|
||||
document_cpf,
|
||||
document_cnpj,
|
||||
regime_tributario,
|
||||
contribuinte_icms,
|
||||
orgao_publico,
|
||||
document_ie,
|
||||
document_im,
|
||||
document_is,
|
||||
end_pais,
|
||||
end_cep,
|
||||
end_ibge,
|
||||
end_logradouro,
|
||||
end_numero,
|
||||
end_complemento,
|
||||
end_bairro,
|
||||
end_cidade,
|
||||
end_uf,
|
||||
info_email,
|
||||
info_email_nfe,
|
||||
info_observacao,
|
||||
info_telefone,
|
||||
info_uso_consumo_ibs_cbs,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM {$this->contatoTable}
|
||||
WHERE {$whereSql}
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->fetchOne(
|
||||
sql: $query,
|
||||
params: $params
|
||||
);
|
||||
|
||||
return $result ? $this->mapToModel($result) : null;
|
||||
}
|
||||
|
||||
private function mapToModel(array $data): ContatoModel {
|
||||
return new ContatoModel(
|
||||
id: $data['id'],
|
||||
uuid: $data['uuid'],
|
||||
empresa_id: $data['empresa_id'],
|
||||
is_active: $data['is_active'],
|
||||
tipo: $data['tipo'],
|
||||
nome_empresarial: $data['nome_empresarial'],
|
||||
nome_fantasia: $data['nome_fantasia'],
|
||||
personalidade: $data['personalidade'],
|
||||
document_cpf: $data['document_cpf'],
|
||||
document_cnpj: $data['document_cnpj'],
|
||||
regime_tributario: $data['regime_tributario'],
|
||||
contribuinte_icms: $data['contribuinte_icms'],
|
||||
orgao_publico: $data['orgao_publico'],
|
||||
document_ie: $data['document_ie'],
|
||||
document_im: $data['document_im'],
|
||||
document_is: $data['document_is'],
|
||||
end_pais: $data['end_pais'],
|
||||
end_cep: $data['end_cep'],
|
||||
end_ibge: $data['end_ibge'],
|
||||
end_logradouro: $data['end_logradouro'],
|
||||
end_numero: $data['end_numero'],
|
||||
end_complemento: $data['end_complemento'],
|
||||
end_bairro: $data['end_bairro'],
|
||||
end_cidade: $data['end_cidade'],
|
||||
end_uf: $data['end_uf'],
|
||||
info_email: $data['info_email'],
|
||||
info_email_nfe: $data['info_email_nfe'],
|
||||
info_observacao: $data['info_observacao'],
|
||||
info_telefone: $data['info_telefone'],
|
||||
info_uso_consumo_ibs_cbs: $data['info_uso_consumo_ibs_cbs'],
|
||||
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
|
||||
updated_at: $data['updated_at'] ? new DateTimeImmutable($data['updated_at']) : null,
|
||||
deleted_at: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
use KrothiumAPI\Http\Router;
|
||||
use WorkbloomERP\Module\v0\Auth\Middlewares\AuthMiddleware;
|
||||
use WorkbloomERP\Module\v0\Contato\Controllers\ContatoController;
|
||||
|
||||
Router::group(
|
||||
prefix: '/contacts',
|
||||
callback: function() {
|
||||
// Endpoint para obter as opções de criação de contato (UF, regime tributário, etc.)
|
||||
Router::get('/create-options', [ContatoController::class, 'createOptions']);
|
||||
|
||||
// Rota para cadastrar um novo contato
|
||||
Router::post('/create', [ContatoController::class, 'create']);
|
||||
},
|
||||
middlewares: [
|
||||
[AuthMiddleware::class, 'handle']
|
||||
]
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Contato\Services;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use WorkbloomERP\Utils\SanitizeUtil;
|
||||
use WorkbloomERP\Utils\ValidateUtil;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
use WorkbloomERP\Module\v0\Auth\Utils\AuthUtil;
|
||||
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Contato\Repos\ContatoRepo;
|
||||
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
|
||||
use WorkbloomERP\Module\v0\Contato\Models\ContatoModel;
|
||||
use WorkbloomERP\Module\v0\Contato\DTOs\ContatoCreateDTO;
|
||||
use WorkbloomERP\Module\v0\Contato\Factories\ContatoServiceFactory;
|
||||
use WorkbloomERP\Constants\SpedCRTConst;
|
||||
use WorkbloomERP\Constants\BrasilUfsConst;
|
||||
use WorkbloomERP\Constants\SpedPaisesConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\OrgaoPublicoConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\PersonalidadeConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\ContribuinteICMSConst;
|
||||
use WorkbloomERP\Module\v0\Contato\Constants\NFSeConsumoIbsCbsConst;
|
||||
|
||||
class ContatoService {
|
||||
public function __construct (
|
||||
protected DBService $db,
|
||||
protected ContatoRepo $contatoRepo,
|
||||
protected EmpresaRepo $empresaRepo,
|
||||
) {}
|
||||
|
||||
public function createOptions(): array {
|
||||
return [
|
||||
'tipo' => PersonalidadeConst::getAll(),
|
||||
'personalidade' => PersonalidadeConst::getAll(),
|
||||
'uf' => BrasilUfsConst::getAll(),
|
||||
'regime_tributario' => SpedCRTConst::getAll(),
|
||||
'contribuinte_icms' => ContribuinteICMSConst::getAll(),
|
||||
'info_uso_consumo_ibs_cbs' => NFSeConsumoIbsCbsConst::getAll(),
|
||||
'orgao_publico' => OrgaoPublicoConst::getAll(),
|
||||
'paises_sped' => SpedPaisesConst::getAll()
|
||||
];
|
||||
}
|
||||
|
||||
public function create(ContatoCreateDTO $contatoCreateDTO): array {
|
||||
try {
|
||||
return $this->db->transaction(
|
||||
callback: function() use ($contatoCreateDTO) {
|
||||
// Pega as informações da sessão para associar o contato à empresa correta
|
||||
$empresaData = AuthUtil::readSession(key: 'empresa_data');
|
||||
|
||||
// Verifica se a empresa existe
|
||||
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'uuid', value: $empresaData['uuid']);
|
||||
|
||||
// Cria a fábrica de contato e valida os dados de criação do contato
|
||||
$contatoFactory = ContatoServiceFactory::makeContratoFactory(empresaModel: $empresaModel, contatoCreateDTO: $contatoCreateDTO);
|
||||
$contatoModel = $contatoFactory->create(contatoCreateDTO: $contatoCreateDTO, empresaModel: $empresaModel);
|
||||
|
||||
return [
|
||||
'response_code' => 201,
|
||||
'message' => 'Contato criado com sucesso.',
|
||||
'output' => ['data' => $contatoModel->toArray()]
|
||||
];
|
||||
}
|
||||
);
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Empresa\Models;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
class EmpresaModel {
|
||||
public function __construct(
|
||||
private ?int $id = null,
|
||||
private ?string $uuid = null,
|
||||
private ?bool $is_active = null,
|
||||
private ?string $nome_empresarial = null,
|
||||
private ?string $nome_fantasia = null,
|
||||
private ?string $tipo = null,
|
||||
private ?int $matriz_id = null,
|
||||
private ?string $document_cnpj = null,
|
||||
private ?string $document_ie = null,
|
||||
private ?string $document_im = null,
|
||||
private ?string $regime_tributario = null,
|
||||
private ?string $end_cep = null,
|
||||
private ?int $end_ibge = null,
|
||||
private ?string $end_logradouro = null,
|
||||
private ?string $end_numero = null,
|
||||
private ?string $end_complemento = null,
|
||||
private ?string $end_bairro = null,
|
||||
private ?string $end_cidade = null,
|
||||
private ?string $end_uf = null,
|
||||
private ?DateTimeImmutable $created_at = null,
|
||||
private ?DateTimeImmutable $updated_at = null,
|
||||
private ?DateTimeImmutable $deleted_at = null,
|
||||
) {}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'uuid' => $this->getUuid(),
|
||||
'is_active' => $this->getIsActive(),
|
||||
'nome_empresarial' => $this->getNomeEmpresarial(),
|
||||
'nome_fantasia' => $this->getNomeFantasia(),
|
||||
'tipo' => $this->getTipo(),
|
||||
'matriz_id' => $this->getMatrizId(),
|
||||
'document_cnpj' => $this->getDocumentCnpj(),
|
||||
'document_ie' => $this->getDocumentIe(),
|
||||
'document_im' => $this->getDocumentIm(),
|
||||
'regime_tributario' => $this->getRegimeTributario(),
|
||||
'end_cep' => $this->getEndCep(),
|
||||
'end_ibge' => $this->getEndIbge(),
|
||||
'end_logradouro' => $this->getEndLogradouro(),
|
||||
'end_numero' => $this->getEndNumero(),
|
||||
'end_complemento' => $this->getEndComplemento(),
|
||||
'end_bairro' => $this->getEndBairro(),
|
||||
'end_cidade' => $this->getEndCidade(),
|
||||
'end_uf' => $this->getEndUf(),
|
||||
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format('Y-m-d H:i:s') : null,
|
||||
'updated_at' => $this->getUpdatedAt() ? $this->getUpdatedAt()->format('Y-m-d H:i:s') : null,
|
||||
'deleted_at' => $this->getDeletedAt() ? $this->getDeletedAt()->format('Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function setId(?int $id): void {
|
||||
$this->id = $id;
|
||||
}
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setUuid(?string $uuid): void {
|
||||
$this->uuid = $uuid;
|
||||
}
|
||||
public function getUuid(): ?string {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function setIsActive(?bool $is_active): void {
|
||||
$this->is_active = $is_active;
|
||||
}
|
||||
public function getIsActive(): ?bool {
|
||||
return $this->is_active;
|
||||
}
|
||||
|
||||
public function setNomeEmpresarial(?string $nome_empresarial): void {
|
||||
$this->nome_empresarial = $nome_empresarial;
|
||||
}
|
||||
public function getNomeEmpresarial(): ?string {
|
||||
return $this->nome_empresarial;
|
||||
}
|
||||
|
||||
public function setNomeFantasia(?string $nome_fantasia): void {
|
||||
$this->nome_fantasia = $nome_fantasia;
|
||||
}
|
||||
public function getNomeFantasia(): ?string {
|
||||
return $this->nome_fantasia;
|
||||
}
|
||||
|
||||
public function setTipo(?string $tipo): void {
|
||||
$this->tipo = $tipo;
|
||||
}
|
||||
public function getTipo(): ?string {
|
||||
return $this->tipo;
|
||||
}
|
||||
|
||||
public function setMatrizId(?int $matriz_id): void {
|
||||
$this->matriz_id = $matriz_id;
|
||||
}
|
||||
public function getMatrizId(): ?int {
|
||||
return $this->matriz_id;
|
||||
}
|
||||
|
||||
public function setDocumentCnpj(?string $document_cnpj): void {
|
||||
$this->document_cnpj = $document_cnpj;
|
||||
}
|
||||
public function getDocumentCnpj(): ?string {
|
||||
return $this->document_cnpj;
|
||||
}
|
||||
|
||||
public function setDocumentIe(?string $document_ie): void {
|
||||
$this->document_ie = $document_ie;
|
||||
}
|
||||
public function getDocumentIe(): ?string {
|
||||
return $this->document_ie;
|
||||
}
|
||||
|
||||
public function setDocumentIm(?string $document_im): void {
|
||||
$this->document_im = $document_im;
|
||||
}
|
||||
public function getDocumentIm(): ?string {
|
||||
return $this->document_im;
|
||||
}
|
||||
|
||||
public function setRegimeTributario(?string $regime_tributario): void {
|
||||
$this->regime_tributario = $regime_tributario;
|
||||
}
|
||||
public function getRegimeTributario(): ?string {
|
||||
return $this->regime_tributario;
|
||||
}
|
||||
|
||||
public function setEndCep(?string $end_cep): void {
|
||||
$this->end_cep = $end_cep;
|
||||
}
|
||||
public function getEndCep(): ?string {
|
||||
return $this->end_cep;
|
||||
}
|
||||
|
||||
public function setEndIbge(?int $end_ibge): void {
|
||||
$this->end_ibge = $end_ibge;
|
||||
}
|
||||
public function getEndIbge(): ?int {
|
||||
return $this->end_ibge;
|
||||
}
|
||||
|
||||
public function setEndLogradouro(?string $end_logradouro): void {
|
||||
$this->end_logradouro = $end_logradouro;
|
||||
}
|
||||
public function getEndLogradouro(): ?string {
|
||||
return $this->end_logradouro;
|
||||
}
|
||||
|
||||
public function setEndNumero(?string $end_numero): void {
|
||||
$this->end_numero = $end_numero;
|
||||
}
|
||||
public function getEndNumero(): ?string {
|
||||
return $this->end_numero;
|
||||
}
|
||||
|
||||
public function setEndComplemento(?string $end_complemento): void {
|
||||
$this->end_complemento = $end_complemento;
|
||||
}
|
||||
public function getEndComplemento(): ?string {
|
||||
return $this->end_complemento;
|
||||
}
|
||||
|
||||
public function setEndBairro(?string $end_bairro): void {
|
||||
$this->end_bairro = $end_bairro;
|
||||
}
|
||||
public function getEndBairro(): ?string {
|
||||
return $this->end_bairro;
|
||||
}
|
||||
|
||||
public function setEndCidade(?string $end_cidade): void {
|
||||
$this->end_cidade = $end_cidade;
|
||||
}
|
||||
public function getEndCidade(): ?string {
|
||||
return $this->end_cidade;
|
||||
}
|
||||
|
||||
public function setEndUf(?string $end_uf): void {
|
||||
$this->end_uf = $end_uf;
|
||||
}
|
||||
public function getEndUf(): ?string {
|
||||
return $this->end_uf;
|
||||
}
|
||||
|
||||
public function setCreatedAt(?DateTimeImmutable $created_at): void {
|
||||
$this->created_at = $created_at;
|
||||
}
|
||||
public function getCreatedAt(): ?DateTimeImmutable {
|
||||
return $this->created_at;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(?DateTimeImmutable $updated_at): void {
|
||||
$this->updated_at = $updated_at;
|
||||
}
|
||||
public function getUpdatedAt(): ?DateTimeImmutable {
|
||||
return $this->updated_at;
|
||||
}
|
||||
|
||||
public function setDeletedAt(?DateTimeImmutable $deleted_at): void {
|
||||
$this->deleted_at = $deleted_at;
|
||||
}
|
||||
public function getDeletedAt(): ?DateTimeImmutable {
|
||||
return $this->deleted_at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Empresa\Repos;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
|
||||
|
||||
class EmpresaRepo {
|
||||
protected string $empresaTable = 'empresa';
|
||||
|
||||
public function __construct(
|
||||
protected DBService $db
|
||||
) {}
|
||||
|
||||
public function insert(EmpresaModel $empresaModel): ?EmpresaModel {
|
||||
$query =
|
||||
"INSERT INTO {$this->empresaTable} (
|
||||
uuid,
|
||||
is_active,
|
||||
nome_empresarial,
|
||||
nome_fantasia,
|
||||
tipo,
|
||||
matriz_id,
|
||||
document_cnpj,
|
||||
document_ie,
|
||||
document_im,
|
||||
regime_tributario,
|
||||
end_cep,
|
||||
end_ibge,
|
||||
end_logradouro,
|
||||
end_numero,
|
||||
end_complemento,
|
||||
end_bairro,
|
||||
end_cidade,
|
||||
end_uf,
|
||||
created_at
|
||||
) VALUES (
|
||||
:uuid,
|
||||
:is_active,
|
||||
:nome_empresarial,
|
||||
:nome_fantasia,
|
||||
:tipo,
|
||||
:matriz_id,
|
||||
:document_cnpj,
|
||||
:document_ie,
|
||||
:document_im,
|
||||
:regime_tributario,
|
||||
:end_cep,
|
||||
:end_ibge,
|
||||
:end_logradouro,
|
||||
:end_numero,
|
||||
:end_complemento,
|
||||
:end_bairro,
|
||||
:end_cidade,
|
||||
:end_uf,
|
||||
:created_at
|
||||
)";
|
||||
|
||||
$empresaModel->setCreatedAt(new DateTimeImmutable());
|
||||
|
||||
$this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':uuid' => $empresaModel->getUuid(),
|
||||
':is_active' => $empresaModel->getIsActive(),
|
||||
':nome_empresarial' => $empresaModel->getNomeEmpresarial(),
|
||||
':nome_fantasia' => $empresaModel->getNomeFantasia(),
|
||||
':tipo' => $empresaModel->getTipo(),
|
||||
':matriz_id' => $empresaModel->getMatrizId(),
|
||||
':document_cnpj' => $empresaModel->getDocumentCnpj(),
|
||||
':document_ie' => $empresaModel->getDocumentIe(),
|
||||
':document_im' => $empresaModel->getDocumentIm(),
|
||||
':regime_tributario' => $empresaModel->getRegimeTributario(),
|
||||
':end_cep' => $empresaModel->getEndCep(),
|
||||
':end_ibge' => $empresaModel->getEndIbge(),
|
||||
':end_logradouro' => $empresaModel->getEndLogradouro(),
|
||||
':end_numero' => $empresaModel->getEndNumero(),
|
||||
':end_complemento' => $empresaModel->getEndComplemento(),
|
||||
':end_bairro' => $empresaModel->getEndBairro(),
|
||||
':end_cidade' => $empresaModel->getEndCidade(),
|
||||
':end_uf' => $empresaModel->getEndUf(),
|
||||
':created_at' => $empresaModel->getCreatedAt()->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
|
||||
$empresaModel->setId($this->db->lastInsertId());
|
||||
return $empresaModel;
|
||||
}
|
||||
|
||||
public function update(EmpresaModel $empresaModel): bool {
|
||||
$query =
|
||||
"UPDATE {$this->empresaTable} SET
|
||||
is_active = :is_active,
|
||||
nome_empresarial = :nome_empresarial,
|
||||
nome_fantasia = :nome_fantasia,
|
||||
tipo = :tipo,
|
||||
matriz_id = :matriz_id,
|
||||
document_cnpj = :document_cnpj,
|
||||
document_ie = :document_ie,
|
||||
document_im = :document_im,
|
||||
regime_tributario = :regime_tributario,
|
||||
end_cep = :end_cep,
|
||||
end_ibge = :end_ibge,
|
||||
end_logradouro = :end_logradouro,
|
||||
end_numero = :end_numero,
|
||||
end_complemento = :end_complemento,
|
||||
end_bairro = :end_bairro,
|
||||
end_cidade = :end_cidade,
|
||||
end_uf = :end_uf,
|
||||
updated_at = :updated_at
|
||||
WHERE id = :id OR uuid = :uuid";
|
||||
|
||||
$empresaModel->setUpdatedAt(new DateTimeImmutable());
|
||||
|
||||
return $this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':id' => $empresaModel->getId(),
|
||||
':uuid' => $empresaModel->getUuid(),
|
||||
':is_active' => $empresaModel->getIsActive(),
|
||||
':nome_empresarial' => $empresaModel->getNomeEmpresarial(),
|
||||
':nome_fantasia' => $empresaModel->getNomeFantasia(),
|
||||
':tipo' => $empresaModel->getTipo(),
|
||||
':matriz_id' => $empresaModel->getMatrizId(),
|
||||
':document_cnpj' => $empresaModel->getDocumentCnpj(),
|
||||
':document_ie' => $empresaModel->getDocumentIe(),
|
||||
':document_im' => $empresaModel->getDocumentIm(),
|
||||
':regime_tributario' => $empresaModel->getRegimeTributario(),
|
||||
':end_cep' => $empresaModel->getEndCep(),
|
||||
':end_ibge' => $empresaModel->getEndIbge(),
|
||||
':end_logradouro' => $empresaModel->getEndLogradouro(),
|
||||
':end_numero' => $empresaModel->getEndNumero(),
|
||||
':end_complemento' => $empresaModel->getEndComplemento(),
|
||||
':end_bairro' => $empresaModel->getEndBairro(),
|
||||
':end_cidade' => $empresaModel->getEndCidade(),
|
||||
':end_uf' => $empresaModel->getEndUf(),
|
||||
':updated_at' => $empresaModel->getUpdatedAt()->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(EmpresaModel $empresaModel): bool {
|
||||
$query =
|
||||
"UPDATE {$this->empresaTable} SET
|
||||
deleted_at = :deleted_at
|
||||
WHERE id = :id OR uuid = :uuid";
|
||||
|
||||
$empresaModel->setDeletedAt(new DateTimeImmutable());
|
||||
|
||||
return $this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':id' => $empresaModel->getId(),
|
||||
':uuid' => $empresaModel->getUuid(),
|
||||
':deleted_at' => $empresaModel->getDeletedAt()->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function findByIdentifier(string $identifier, mixed $value): ?EmpresaModel {
|
||||
$query =
|
||||
"SELECT
|
||||
id,
|
||||
uuid,
|
||||
is_active,
|
||||
nome_empresarial,
|
||||
nome_fantasia,
|
||||
tipo,
|
||||
matriz_id,
|
||||
document_cnpj,
|
||||
document_ie,
|
||||
document_im,
|
||||
regime_tributario,
|
||||
end_cep,
|
||||
end_ibge,
|
||||
end_logradouro,
|
||||
end_numero,
|
||||
end_complemento,
|
||||
end_bairro,
|
||||
end_cidade,
|
||||
end_uf,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM {$this->empresaTable}
|
||||
WHERE $identifier = :value
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->fetchOne(
|
||||
sql: $query,
|
||||
params: [
|
||||
':value' => $value
|
||||
]
|
||||
);
|
||||
|
||||
return $result ? $this->mapToModel($result) : null;
|
||||
}
|
||||
|
||||
public function findAllByMatrizId(int $matriz_id): array {
|
||||
$query =
|
||||
"SELECT
|
||||
id,
|
||||
uuid,
|
||||
is_active,
|
||||
nome_empresarial,
|
||||
nome_fantasia,
|
||||
tipo,
|
||||
matriz_id,
|
||||
document_cnpj,
|
||||
document_ie,
|
||||
document_im,
|
||||
regime_tributario,
|
||||
end_cep,
|
||||
end_ibge,
|
||||
end_logradouro,
|
||||
end_numero,
|
||||
end_complemento,
|
||||
end_bairro,
|
||||
end_cidade,
|
||||
end_uf,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM {$this->empresaTable}
|
||||
WHERE (
|
||||
id = :matriz_id OR
|
||||
matriz_id = :matriz_id
|
||||
)
|
||||
AND deleted_at IS NULL";
|
||||
|
||||
$results = $this->db->fetchAll(
|
||||
sql: $query,
|
||||
params: [
|
||||
':matriz_id' => $matriz_id
|
||||
]
|
||||
);
|
||||
|
||||
return array_map(fn($data) => $this->mapToModel($data), $results);
|
||||
}
|
||||
|
||||
private function mapToModel(array $data): EmpresaModel {
|
||||
return new EmpresaModel(
|
||||
id: $data['id'],
|
||||
uuid: $data['uuid'],
|
||||
is_active: $data['is_active'],
|
||||
nome_empresarial: $data['nome_empresarial'],
|
||||
nome_fantasia: $data['nome_fantasia'],
|
||||
tipo: $data['tipo'],
|
||||
matriz_id: $data['matriz_id'],
|
||||
document_cnpj: $data['document_cnpj'],
|
||||
document_ie: $data['document_ie'],
|
||||
document_im: $data['document_im'],
|
||||
regime_tributario: $data['regime_tributario'],
|
||||
end_cep: $data['end_cep'],
|
||||
end_ibge: $data['end_ibge'],
|
||||
end_logradouro: $data['end_logradouro'],
|
||||
end_numero: $data['end_numero'],
|
||||
end_complemento: $data['end_complemento'],
|
||||
end_bairro: $data['end_bairro'],
|
||||
end_cidade: $data['end_cidade'],
|
||||
end_uf: $data['end_uf'],
|
||||
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
|
||||
updated_at: $data['updated_at'] ? new DateTimeImmutable($data['updated_at']) : null,
|
||||
deleted_at: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Usuario\Models;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
class UsuarioEmpresaModel {
|
||||
public function __construct(
|
||||
private ?int $usuario_id = null,
|
||||
private ?int $empresa_id = null,
|
||||
) {}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'usuario_id' => $this->getUsuarioId(),
|
||||
'empresa_id' => $this->getEmpresaId(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getUsuarioId(): ?int {
|
||||
return $this->usuario_id;
|
||||
}
|
||||
public function setUsuarioId(?int $usuario_id): void {
|
||||
$this->usuario_id = $usuario_id;
|
||||
}
|
||||
|
||||
public function getEmpresaId(): ?int {
|
||||
return $this->empresa_id;
|
||||
}
|
||||
public function setEmpresaId(?int $empresa_id): void {
|
||||
$this->empresa_id = $empresa_id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Usuario\Models;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
class UsuarioModel {
|
||||
public function __construct(
|
||||
private ?int $id = null,
|
||||
private ?string $uuid = null,
|
||||
private ?bool $is_active = null,
|
||||
private ?bool $is_root = null,
|
||||
private ?string $nome_completo = null,
|
||||
private ?string $nome_usuario = null,
|
||||
private ?string $email = null,
|
||||
private ?string $senha_hash = null,
|
||||
private ?DateTimeImmutable $created_at = null,
|
||||
private ?DateTimeImmutable $updated_at = null,
|
||||
private ?DateTimeImmutable $deleted_at = null,
|
||||
) {}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'uuid' => $this->getUuid(),
|
||||
'is_active' => $this->getIsActive(),
|
||||
'is_root' => $this->getIsRoot(),
|
||||
'nome_completo' => $this->getNomeCompleto(),
|
||||
'nome_usuario' => $this->getNomeUsuario(),
|
||||
'email' => $this->getEmail(),
|
||||
'senha_hash' => $this->getSenhaHash(),
|
||||
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format('Y-m-d H:i:s') : null,
|
||||
'updated_at' => $this->getUpdatedAt() ? $this->getUpdatedAt()->format('Y-m-d H:i:s') : null,
|
||||
'deleted_at' => $this->getDeletedAt() ? $this->getDeletedAt()->format('Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function setId(?int $id): void {
|
||||
$this->id = $id;
|
||||
}
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setUuid(?string $uuid): void {
|
||||
$this->uuid = $uuid;
|
||||
}
|
||||
public function getUuid(): ?string {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function setIsActive(?bool $is_active): void {
|
||||
$this->is_active = $is_active;
|
||||
}
|
||||
public function getIsActive(): ?bool {
|
||||
return $this->is_active;
|
||||
}
|
||||
|
||||
public function setIsRoot(?bool $is_root): void {
|
||||
$this->is_root = $is_root;
|
||||
}
|
||||
public function getIsRoot(): ?bool {
|
||||
return $this->is_root;
|
||||
}
|
||||
|
||||
public function setNomeCompleto(?string $nome_completo): void {
|
||||
$this->nome_completo = $nome_completo;
|
||||
}
|
||||
public function getNomeCompleto(): ?string {
|
||||
return $this->nome_completo;
|
||||
}
|
||||
|
||||
public function setNomeUsuario(?string $nome_usuario): void {
|
||||
$this->nome_usuario = $nome_usuario;
|
||||
}
|
||||
public function getNomeUsuario(): ?string {
|
||||
return $this->nome_usuario;
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): void {
|
||||
$this->email = $email;
|
||||
}
|
||||
public function getEmail(): ?string {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setSenhaHash(?string $senha_hash): void {
|
||||
$this->senha_hash = $senha_hash;
|
||||
}
|
||||
public function getSenhaHash(): ?string {
|
||||
return $this->senha_hash;
|
||||
}
|
||||
|
||||
public function setCreatedAt(?DateTimeImmutable $created_at): void {
|
||||
$this->created_at = $created_at;
|
||||
}
|
||||
public function getCreatedAt(): ?DateTimeImmutable {
|
||||
return $this->created_at;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(?DateTimeImmutable $updated_at): void {
|
||||
$this->updated_at = $updated_at;
|
||||
}
|
||||
public function getUpdatedAt(): ?DateTimeImmutable {
|
||||
return $this->updated_at;
|
||||
}
|
||||
|
||||
public function setDeletedAt(?DateTimeImmutable $deleted_at): void {
|
||||
$this->deleted_at = $deleted_at;
|
||||
}
|
||||
public function getDeletedAt(): ?DateTimeImmutable {
|
||||
return $this->deleted_at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Usuario\Models;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
class UsuarioSessionModel {
|
||||
public function __construct(
|
||||
private ?int $id = null,
|
||||
private ?string $uuid = null,
|
||||
private ?int $usuario_id = null,
|
||||
private ?string $user_agent = null,
|
||||
private ?string $ip_address = null,
|
||||
private ?string $token_hash = null,
|
||||
private ?DateTimeImmutable $created_at = null,
|
||||
private ?DateTimeImmutable $revoked_at = null,
|
||||
) {}
|
||||
|
||||
public function toArray(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'uuid' => $this->getUuid(),
|
||||
'usuario_id' => $this->getUsuarioId(),
|
||||
'user_agent' => $this->getUserAgent(),
|
||||
'ip_address' => $this->getIpAddress(),
|
||||
'token_hash' => $this->getTokenHash(),
|
||||
'created_at' => $this->getCreatedAt() ? $this->getCreatedAt()->format('Y-m-d H:i:s') : null,
|
||||
'revoked_at' => $this->getRevokedAt() ? $this->getRevokedAt()->format('Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function setId(?int $id): void {
|
||||
$this->id = $id;
|
||||
}
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setUuid(?string $uuid): void {
|
||||
$this->uuid = $uuid;
|
||||
}
|
||||
public function getUuid(): ?string {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function setUsuarioId(?int $usuario_id): void {
|
||||
$this->usuario_id = $usuario_id;
|
||||
}
|
||||
public function getUsuarioId(): ?int {
|
||||
return $this->usuario_id;
|
||||
}
|
||||
|
||||
public function setUserAgent(?string $user_agent): void {
|
||||
$this->user_agent = $user_agent;
|
||||
}
|
||||
public function getUserAgent(): ?string {
|
||||
return $this->user_agent;
|
||||
}
|
||||
|
||||
public function setIpAddress(?string $ip_address): void {
|
||||
$this->ip_address = $ip_address;
|
||||
}
|
||||
public function getIpAddress(): ?string {
|
||||
return $this->ip_address;
|
||||
}
|
||||
|
||||
public function setTokenHash(?string $token_hash): void {
|
||||
$this->token_hash = $token_hash;
|
||||
}
|
||||
public function getTokenHash(): ?string {
|
||||
return $this->token_hash;
|
||||
}
|
||||
|
||||
public function setCreatedAt(?DateTimeImmutable $created_at): void {
|
||||
$this->created_at = $created_at;
|
||||
}
|
||||
public function getCreatedAt(): ?DateTimeImmutable {
|
||||
return $this->created_at;
|
||||
}
|
||||
|
||||
public function setRevokedAt(?DateTimeImmutable $revoked_at): void {
|
||||
$this->revoked_at = $revoked_at;
|
||||
}
|
||||
public function getRevokedAt(): ?DateTimeImmutable {
|
||||
return $this->revoked_at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Usuario\Repos;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioEmpresaModel;
|
||||
|
||||
class UsuarioEmpresaRepo {
|
||||
protected string $usuarioEmpresaTable = 'shared.usuario_empresa';
|
||||
|
||||
public function __construct(
|
||||
private DBService $db
|
||||
) {}
|
||||
|
||||
public function insert(UsuarioEmpresaModel $usuarioEmpresaModel): bool {
|
||||
$query =
|
||||
"INSERT INTO {$this->usuarioEmpresaTable} (
|
||||
usuario_id,
|
||||
empresa_id
|
||||
) VALUES (
|
||||
:usuario_id,
|
||||
:empresa_id
|
||||
)";
|
||||
|
||||
return $this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
'usuario_id' => $usuarioEmpresaModel->getUsuarioId(),
|
||||
'empresa_id' => $usuarioEmpresaModel->getEmpresaId()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(UsuarioEmpresaModel $usuarioEmpresaModel): bool {
|
||||
$query =
|
||||
"DELETE FROM {$this->usuarioEmpresaTable} WHERE usuario_id = :usuario_id AND empresa_id = :empresa_id";
|
||||
|
||||
return $this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
'usuario_id' => $usuarioEmpresaModel->getUsuarioId(),
|
||||
'empresa_id' => $usuarioEmpresaModel->getEmpresaId()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function findAllByUsuarioId(int $usuario_id): array {
|
||||
$query =
|
||||
"SELECT
|
||||
usuario_id,
|
||||
empresa_id
|
||||
FROM {$this->usuarioEmpresaTable}
|
||||
WHERE usuario_id = :usuario_id";
|
||||
|
||||
return $this->db->fetchAll(
|
||||
sql: $query,
|
||||
params: [
|
||||
'usuario_id' => $usuario_id
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function checkAssociationByUsuarioIdAndEmpresaId(int $usuario_id, int $empresa_id): bool {
|
||||
$query =
|
||||
"SELECT 1 FROM {$this->usuarioEmpresaTable} WHERE usuario_id = :usuario_id AND empresa_id = :empresa_id";
|
||||
|
||||
$result = $this->db->fetchOne(
|
||||
sql: $query,
|
||||
params: [
|
||||
'usuario_id' => $usuario_id,
|
||||
'empresa_id' => $empresa_id
|
||||
]
|
||||
);
|
||||
|
||||
return !empty($result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Usuario\Repos;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
|
||||
|
||||
class UsuarioRepo {
|
||||
protected string $usuarioTable = 'usuario';
|
||||
|
||||
public function __construct(
|
||||
private DBService $db
|
||||
) {}
|
||||
|
||||
public function insert(UsuarioModel $usuarioModel): ?UsuarioModel {
|
||||
$query =
|
||||
"INSERT INTO {$this->usuarioTable} (
|
||||
uuid,
|
||||
is_active,
|
||||
is_root,
|
||||
nome_completo,
|
||||
nome_usuario,
|
||||
email,
|
||||
senha_hash,
|
||||
created_at
|
||||
) VALUES (
|
||||
:uuid,
|
||||
:is_active,
|
||||
:is_root,
|
||||
:nome_completo,
|
||||
:nome_usuario,
|
||||
:email,
|
||||
:senha_hash,
|
||||
:created_at
|
||||
)";
|
||||
|
||||
$usuarioModel->setCreatedAt(created_at: new DateTimeImmutable());
|
||||
|
||||
$this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
'uuid' => $usuarioModel->getUuid(),
|
||||
'is_active' => $usuarioModel->getIsActive(),
|
||||
'is_root' => $usuarioModel->getIsRoot(),
|
||||
'nome_completo' => $usuarioModel->getNomeCompleto(),
|
||||
'nome_usuario' => $usuarioModel->getNomeUsuario(),
|
||||
'email' => $usuarioModel->getEmail(),
|
||||
'senha_hash' => $usuarioModel->getSenhaHash(),
|
||||
'created_at' => $usuarioModel->getCreatedAt()->format(format: 'Y-m-d H:i:s')
|
||||
]
|
||||
);
|
||||
|
||||
$usuarioModel->setId(id: $this->db->lastInsertId());
|
||||
return $usuarioModel;
|
||||
}
|
||||
|
||||
public function update(UsuarioModel $usuarioModel): ?UsuarioModel {
|
||||
$query =
|
||||
"UPDATE {$this->usuarioTable} SET
|
||||
is_active = :is_active,
|
||||
is_root = :is_root,
|
||||
nome_completo = :nome_completo,
|
||||
nome_usuario = :nome_usuario,
|
||||
email = :email,
|
||||
senha_hash = :senha_hash,
|
||||
updated_at = :updated_at
|
||||
WHERE id = :id OR uuid = :uuid";
|
||||
|
||||
$usuarioModel->setUpdatedAt(updated_at: new DateTimeImmutable());
|
||||
$this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':id' => $usuarioModel->getId(),
|
||||
':uuid' => $usuarioModel->getUuid(),
|
||||
':is_active' => $usuarioModel->getIsActive(),
|
||||
':is_root' => $usuarioModel->getIsRoot(),
|
||||
':nome_completo' => $usuarioModel->getNomeCompleto(),
|
||||
':nome_usuario' => $usuarioModel->getNomeUsuario(),
|
||||
':email' => $usuarioModel->getEmail(),
|
||||
':senha_hash' => $usuarioModel->getSenhaHash(),
|
||||
':updated_at' => $usuarioModel->getUpdatedAt()->format(format: 'Y-m-d H:i:s')
|
||||
]
|
||||
);
|
||||
|
||||
return $usuarioModel;
|
||||
}
|
||||
|
||||
public function delete(UsuarioModel $usuarioModel): ?UsuarioModel {
|
||||
$query =
|
||||
"UPDATE {$this->usuarioTable} SET
|
||||
is_active = 0,
|
||||
deleted_at = :deleted_at
|
||||
WHERE id = :id OR uuid = :uuid";
|
||||
|
||||
$usuarioModel->setDeletedAt(deleted_at: new DateTimeImmutable());
|
||||
|
||||
$this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':id' => $usuarioModel->getId(),
|
||||
':uuid' => $usuarioModel->getUuid(),
|
||||
':deleted_at' => $usuarioModel->getDeletedAt()->format(format: 'Y-m-d H:i:s')
|
||||
]
|
||||
);
|
||||
|
||||
return $usuarioModel;
|
||||
}
|
||||
|
||||
public function findByIdentifier(string $identifier, mixed $value): ?UsuarioModel {
|
||||
$query =
|
||||
"SELECT
|
||||
id,
|
||||
uuid,
|
||||
is_active,
|
||||
is_root,
|
||||
nome_completo,
|
||||
nome_usuario,
|
||||
email,
|
||||
senha_hash,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM {$this->usuarioTable}
|
||||
WHERE {$identifier} = :value
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->fetchOne(
|
||||
sql: $query,
|
||||
params: [
|
||||
':value' => $value
|
||||
]
|
||||
);
|
||||
|
||||
return $result ? $this->mapToModel($result) : null;
|
||||
}
|
||||
|
||||
public function findByLogin(string $login): ?UsuarioModel {
|
||||
$query =
|
||||
"SELECT
|
||||
id,
|
||||
uuid,
|
||||
is_active,
|
||||
is_root,
|
||||
nome_completo,
|
||||
nome_usuario,
|
||||
email,
|
||||
senha_hash,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM {$this->usuarioTable}
|
||||
WHERE (nome_usuario = :login OR email = :login)
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->fetchOne(
|
||||
sql: $query,
|
||||
params: [
|
||||
':login' => $login
|
||||
]
|
||||
);
|
||||
|
||||
return $result ? $this->mapToModel($result) : null;
|
||||
}
|
||||
|
||||
private function mapToModel(array $data): UsuarioModel {
|
||||
return new UsuarioModel(
|
||||
id: $data['id'],
|
||||
uuid: $data['uuid'],
|
||||
is_active: $data['is_active'],
|
||||
is_root: $data['is_root'],
|
||||
nome_completo: $data['nome_completo'],
|
||||
nome_usuario: $data['nome_usuario'],
|
||||
email: $data['email'],
|
||||
senha_hash: $data['senha_hash'],
|
||||
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
|
||||
updated_at: $data['updated_at'] ? new DateTimeImmutable($data['updated_at']) : null,
|
||||
deleted_at: $data['deleted_at'] ? new DateTimeImmutable($data['deleted_at']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Usuario\Repos;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
|
||||
|
||||
class UsuarioSessionRepo {
|
||||
protected string $usuarioSessionTable = 'usuario_session';
|
||||
|
||||
public function __construct(
|
||||
private DBService $db
|
||||
) {}
|
||||
|
||||
public function insert(UsuarioSessionModel $usuarioSessionModel): ?UsuarioSessionModel {
|
||||
$query =
|
||||
"INSERT INTO {$this->usuarioSessionTable} (
|
||||
uuid,
|
||||
usuario_id,
|
||||
user_agent,
|
||||
ip_address,
|
||||
token_hash,
|
||||
created_at
|
||||
) VALUES (
|
||||
:uuid,
|
||||
:usuario_id,
|
||||
:user_agent,
|
||||
:ip_address,
|
||||
:token_hash,
|
||||
:created_at
|
||||
)";
|
||||
|
||||
$usuarioSessionModel->setCreatedAt(new DateTimeImmutable());
|
||||
|
||||
$this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':uuid' => $usuarioSessionModel->getUuid(),
|
||||
':usuario_id' => $usuarioSessionModel->getUsuarioId(),
|
||||
':user_agent' => $usuarioSessionModel->getUserAgent(),
|
||||
':ip_address' => $usuarioSessionModel->getIpAddress(),
|
||||
':token_hash' => $usuarioSessionModel->getTokenHash(),
|
||||
':created_at' => $usuarioSessionModel->getCreatedAt()->format('Y-m-d H:i:s')
|
||||
]
|
||||
);
|
||||
|
||||
$usuarioSessionModel->setId($this->db->lastInsertId());
|
||||
return $usuarioSessionModel;
|
||||
}
|
||||
|
||||
public function update(UsuarioSessionModel $usuarioSessionModel): bool {
|
||||
$query =
|
||||
"UPDATE {$this->usuarioSessionTable} SET
|
||||
usuario_id = :usuario_id,
|
||||
user_agent = :user_agent,
|
||||
ip_address = :ip_address,
|
||||
token_hash = :token_hash,
|
||||
revoked_at = :revoked_at
|
||||
WHERE id = :id OR uuid = :uuid";
|
||||
|
||||
return $this->db->execute(
|
||||
sql: $query,
|
||||
params: [
|
||||
':id' => $usuarioSessionModel->getId(),
|
||||
':uuid' => $usuarioSessionModel->getUuid(),
|
||||
':usuario_id' => $usuarioSessionModel->getUsuarioId(),
|
||||
':user_agent' => $usuarioSessionModel->getUserAgent(),
|
||||
':ip_address' => $usuarioSessionModel->getIpAddress(),
|
||||
':token_hash' => $usuarioSessionModel->getTokenHash(),
|
||||
':revoked_at' => $usuarioSessionModel->getRevokedAt() ? $usuarioSessionModel->getRevokedAt()->format('Y-m-d H:i:s') : null
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function findByIdentifier(string $identifier, mixed $value): ?UsuarioSessionModel {
|
||||
$query =
|
||||
"SELECT
|
||||
id,
|
||||
uuid,
|
||||
usuario_id,
|
||||
user_agent,
|
||||
ip_address,
|
||||
token_hash,
|
||||
created_at,
|
||||
revoked_at
|
||||
FROM {$this->usuarioSessionTable}
|
||||
WHERE $identifier = :value
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->fetchOne(
|
||||
sql: $query,
|
||||
params: [':value' => $value]
|
||||
);
|
||||
|
||||
return $result ? $this->mapToModel($result) : null;
|
||||
}
|
||||
|
||||
private function mapToModel(array $data) {
|
||||
return new UsuarioSessionModel(
|
||||
id: $data['id'],
|
||||
uuid: $data['uuid'],
|
||||
usuario_id: $data['usuario_id'],
|
||||
user_agent: $data['user_agent'],
|
||||
ip_address: $data['ip_address'],
|
||||
token_hash: $data['token_hash'],
|
||||
created_at: $data['created_at'] ? new DateTimeImmutable($data['created_at']) : null,
|
||||
revoked_at: $data['revoked_at'] ? new DateTimeImmutable($data['revoked_at']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Constants;
|
||||
|
||||
class BrasilUfsConst {
|
||||
public const UFS = [
|
||||
'AC' => 'Acre',
|
||||
'AL' => 'Alagoas',
|
||||
'AP' => 'Amapá',
|
||||
'AM' => 'Amazonas',
|
||||
'BA' => 'Bahia',
|
||||
'CE' => 'Ceará',
|
||||
'DF' => 'Distrito Federal',
|
||||
'ES' => 'Espírito Santo',
|
||||
'EX' => 'Estrangeiro',
|
||||
'GO' => 'Goiás',
|
||||
'MA' => 'Maranhão',
|
||||
'MT' => 'Mato Grosso',
|
||||
'MS' => 'Mato Grosso do Sul',
|
||||
'MG' => 'Minas Gerais',
|
||||
'PA' => 'Pará',
|
||||
'PB' => 'Paraíba',
|
||||
'PR' => 'Paraná',
|
||||
'PE' => 'Pernambuco',
|
||||
'PI' => 'Piauí',
|
||||
'RJ' => 'Rio de Janeiro',
|
||||
'RN' => 'Rio Grande do Norte',
|
||||
'RS' => 'Rio Grande do Sul',
|
||||
'RO' => 'Rondônia',
|
||||
'RR' => 'Roraima',
|
||||
'SC' => 'Santa Catarina',
|
||||
'SP' => 'São Paulo',
|
||||
'SE' => 'Sergipe',
|
||||
'TO' => 'Tocantins'
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::UFS;
|
||||
}
|
||||
|
||||
public static function exists(string $uf): bool {
|
||||
return isset(self::UFS[strtoupper($uf)]);
|
||||
}
|
||||
|
||||
public static function getName(string $uf): ?string {
|
||||
return self::UFS[strtoupper($uf)] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Constants;
|
||||
|
||||
class SpedCRTConst {
|
||||
public const CRT = [
|
||||
"1" => "Simples Nacional",
|
||||
"2" => "Simples Nacional - Excesso de Sublimite de Receita Bruta",
|
||||
"3" => "Regime Normal",
|
||||
"4" => "MEI - Microempreendedor Individual"
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::CRT;
|
||||
}
|
||||
|
||||
public static function getDescription(string|int|null $code): string {
|
||||
return self::CRT[$code] ?? 'Código de regime tributário desconhecido.';
|
||||
}
|
||||
|
||||
public static function exists(string|int|null $code): bool {
|
||||
return isset(self::CRT[$code]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Constants;
|
||||
|
||||
class SpedPaisesConst {
|
||||
public const PAISES = [
|
||||
"0132" => "AFEGANISTAO",
|
||||
"7560" => "AFRICA DO SUL",
|
||||
"0175" => "ALBANIA",
|
||||
"0230" => "ALEMANHA",
|
||||
"0370" => "ANDORRA",
|
||||
"0400" => "ANGOLA",
|
||||
"0418" => "ANGUILLA",
|
||||
"0434" => "ANTIGUA E BARBUDA",
|
||||
"0531" => "ARABIA SAUDITA",
|
||||
"0590" => "ARGELIA",
|
||||
"0639" => "ARGENTINA",
|
||||
"0647" => "ARMENIA",
|
||||
"0655" => "ARUBA",
|
||||
"0698" => "AUSTRALIA",
|
||||
"0728" => "AUSTRIA",
|
||||
"0736" => "AZERBAIDJAO",
|
||||
"0779" => "BAHAMAS, ILHAS",
|
||||
"0817" => "BANGLADESH",
|
||||
"0833" => "BARBADOS",
|
||||
"0809" => "BAREIN",
|
||||
"0850" => "BELARUS",
|
||||
"0876" => "BELGICA",
|
||||
"0884" => "BELIZE",
|
||||
"2291" => "BENIN",
|
||||
"0906" => "BERMUDAS",
|
||||
"0973" => "BOLIVIA",
|
||||
"0990" => "BONAIRE, SAINT EUSTATIUS E SABA",
|
||||
"0981" => "BOSNIA-HERZEGOVINA",
|
||||
"1015" => "BOTSUANA",
|
||||
"1098" => "BOUVET, ILHA",
|
||||
"1058" => "BRASIL",
|
||||
"1082" => "BRUNEI",
|
||||
"1112" => "BULGARIA",
|
||||
"0310" => "BURKINA FASO",
|
||||
"1155" => "BURUNDI",
|
||||
"1198" => "BUTAO",
|
||||
"1279" => "CABO VERDE",
|
||||
"1457" => "CAMAROES",
|
||||
"1414" => "CAMBOJA",
|
||||
"1490" => "CANADA",
|
||||
"1546" => "CATAR",
|
||||
"1538" => "CAZAQUISTAO",
|
||||
"7889" => "CHADE",
|
||||
"1589" => "CHILE",
|
||||
"1600" => "CHINA",
|
||||
"1635" => "CHIPRE",
|
||||
"5118" => "CHRISTMAS, ILHA (NAVIDAD)",
|
||||
"7412" => "CINGAPURA",
|
||||
"1651" => "COCOS (KEELINGS)",
|
||||
"1694" => "COLOMBIA",
|
||||
"1775" => "CONGO",
|
||||
"1872" => "COREIA DO NORTE",
|
||||
"1937" => "COSTA DO MARFIM",
|
||||
"1376" => "CAYMAN",
|
||||
"1732" => "COMORES",
|
||||
"1830" => "COOK",
|
||||
"1902" => "COREIA DO SUL",
|
||||
"1961" => "COSTA RICA",
|
||||
"1953" => "CROACIA",
|
||||
"1996" => "CUBA",
|
||||
"2003" => "CURACAO",
|
||||
"2321" => "DINAMARCA",
|
||||
"7838" => "DJIBUTI",
|
||||
"2356" => "DOMINICA",
|
||||
"2402" => "EGITO",
|
||||
"6874" => "EL SALVADOR",
|
||||
"2445" => "EMIRADOS ARABES UNIDOS",
|
||||
"2399" => "EQUADOR",
|
||||
"2437" => "ERITREIA",
|
||||
"2470" => "ESLOVAQUIA",
|
||||
"2461" => "ESLOVENIA",
|
||||
"2453" => "ESPANHA",
|
||||
"2496" => "ESTADOS UNIDOS",
|
||||
"2518" => "ESTONIA",
|
||||
"7544" => "ESWATINI (ANTIGA SUAZILANDIA)",
|
||||
"2534" => "ETIOPIA",
|
||||
"2550" => "FALKLAND (MALVINAS)",
|
||||
"2593" => "FAROE",
|
||||
"8702" => "FIJI",
|
||||
"2674" => "FILIPINAS",
|
||||
"2712" => "FINLANDIA",
|
||||
"2755" => "FRANCA",
|
||||
"2810" => "GABAO",
|
||||
"2852" => "GAMBIA",
|
||||
"2895" => "GANA",
|
||||
"2917" => "GEORGIA",
|
||||
"2933" => "GIBRALTAR",
|
||||
"2976" => "GRANADA",
|
||||
"3018" => "GRECIA",
|
||||
"3050" => "GROENLANDIA",
|
||||
"3093" => "GUADALUPE",
|
||||
"3131" => "GUAM",
|
||||
"3174" => "GUATEMALA",
|
||||
"1504" => "GUERNSEY, ILHA DO CANAL (INCLUI ALDERNEY E SARK)",
|
||||
"3379" => "GUIANA",
|
||||
"3255" => "GUIANA FRANCESA",
|
||||
"3298" => "GUINE",
|
||||
"3344" => "GUINE-BISSAU",
|
||||
"3310" => "GUINE-EQUATORIAL",
|
||||
"3417" => "HAITI",
|
||||
"5738" => "HOLANDA (PAISES BAIXOS)",
|
||||
"3450" => "HONDURAS",
|
||||
"3514" => "HONG KONG",
|
||||
"3557" => "HUNGRIA",
|
||||
"3573" => "IEMEN",
|
||||
"3611" => "INDIA",
|
||||
"3654" => "INDONESIA",
|
||||
"3727" => "IRA",
|
||||
"3697" => "IRAQUE",
|
||||
"3751" => "IRLANDA",
|
||||
"3794" => "ISLANDIA",
|
||||
"3832" => "ISRAEL",
|
||||
"3867" => "ITALIA",
|
||||
"3913" => "JAMAICA",
|
||||
"3999" => "JAPAO",
|
||||
"1508" => "JERSEY, ILHA DO CANAL",
|
||||
"3964" => "JOHNSTON",
|
||||
"4030" => "JORDANIA",
|
||||
"4111" => "KIRIBATI",
|
||||
"1988" => "KUWEIT (ou Coveite)",
|
||||
"4200" => "LAOS",
|
||||
"4260" => "LESOTO",
|
||||
"4278" => "LETONIA",
|
||||
"4316" => "LIBANO",
|
||||
"4340" => "LIBERIA",
|
||||
"4383" => "LIBIA",
|
||||
"4405" => "LIECHTENSTEIN",
|
||||
"4421" => "LITUANIA",
|
||||
"4456" => "LUXEMBURGO",
|
||||
"4472" => "MACAU",
|
||||
"4499" => "MACEDONIA",
|
||||
"4502" => "MADAGASCAR",
|
||||
"4553" => "MALASIA",
|
||||
"4588" => "MALAVI",
|
||||
"4618" => "MALDIVAS",
|
||||
"4642" => "MALI",
|
||||
"4677" => "MALTA",
|
||||
"3595" => "MAN, ILHA DE",
|
||||
"4723" => "MARIANAS DO NORTE",
|
||||
"4740" => "MARROCOS",
|
||||
"4766" => "MARSHALL, ILHAS",
|
||||
"4774" => "MARTINICA",
|
||||
"4855" => "MAURICIO",
|
||||
"4880" => "MAURITANIA",
|
||||
"4936" => "MEXICO",
|
||||
"0930" => "MIANMAR",
|
||||
"4995" => "MICRONESIA",
|
||||
"5053" => "MOCAMBIQUE",
|
||||
"4944" => "MOLDAVIA",
|
||||
"4952" => "MONACO",
|
||||
"4979" => "MONGOLIA",
|
||||
"4985" => "MONTENEGRO",
|
||||
"5010" => "MONTSERRAT",
|
||||
"5070" => "NAMIBIA",
|
||||
"5088" => "NAURU",
|
||||
"5177" => "NEPAL",
|
||||
"5215" => "NICARAGUA",
|
||||
"5258" => "NIGER",
|
||||
"5282" => "NIGERIA",
|
||||
"5312" => "NIUE",
|
||||
"5355" => "NORFOLK, ILHA",
|
||||
"5380" => "NORUEGA",
|
||||
"5428" => "NOVA CALEDONIA",
|
||||
"5487" => "NOVA ZELANDIA",
|
||||
"5568" => "OMA",
|
||||
"5665" => "PACIFICO, ILHAS DO (POSSESSAO DOS EUA)",
|
||||
"5754" => "PALAU",
|
||||
"5780" => "PALESTINA",
|
||||
"5800" => "PANAMA",
|
||||
"5452" => "PAPUA NOVA GUINE",
|
||||
"5762" => "PAQUISTAO",
|
||||
"5860" => "PARAGUAI",
|
||||
"5894" => "PERU",
|
||||
"5932" => "PITCAIRN",
|
||||
"5991" => "POLINESIA FRANCESA",
|
||||
"6033" => "POLONIA",
|
||||
"6114" => "PORTO RICO",
|
||||
"6076" => "PORTUGAL",
|
||||
"6238" => "QUENIA",
|
||||
"6254" => "QUIRGUISTAO",
|
||||
"6289" => "REINO UNIDO",
|
||||
"6408" => "REPUBLICA CENTRO-AFRICANA",
|
||||
"8885" => "REPUBLICA DEMOCRATICA DO CONGO",
|
||||
"6475" => "REPUBLICA DOMINICANA",
|
||||
"7919" => "REPUBLICA TCHECA",
|
||||
"6602" => "REUNIAO",
|
||||
"6700" => "ROMENIA",
|
||||
"6750" => "RUANDA",
|
||||
"6769" => "RUSSIA",
|
||||
"6858" => "SAARA OCIDENTAL",
|
||||
"6777" => "SALOMAO, ILHAS",
|
||||
"6904" => "SAMOA",
|
||||
"6912" => "SAMOA AMERICANA",
|
||||
"6971" => "SAN MARINO",
|
||||
"7102" => "SANTA HELENA",
|
||||
"7153" => "SANTA LUCIA",
|
||||
"6955" => "SAO CRISTOVAO E NEVES",
|
||||
"6980" => "SAO MARTINHO, ILHA DE (PARTE FRANCESA)",
|
||||
"6998" => "SAO MARTINHO, ILHA DE (PARTE HOLANDESA)",
|
||||
"7005" => "SAO PEDRO E MIQUELON",
|
||||
"7200" => "SAO TOME E PRINCIPE",
|
||||
"7056" => "SAO VICENTE E GRANADINAS",
|
||||
"7315" => "SEICHELES",
|
||||
"7285" => "SENEGAL",
|
||||
"7358" => "SERRA LEOA",
|
||||
"7370" => "SERVIA",
|
||||
"7447" => "SIRIA",
|
||||
"7480" => "SOMALIA",
|
||||
"7501" => "SRI LANKA",
|
||||
"7595" => "SUDAO",
|
||||
"7600" => "SUDÃO DO SUL",
|
||||
"7641" => "SUECIA",
|
||||
"7676" => "SUICA",
|
||||
"7706" => "SURINAME",
|
||||
"7552" => "SVALBARD E JAN MAYEN",
|
||||
"7722" => "TADJIQUISTAO",
|
||||
"7765" => "TAILANDIA",
|
||||
"1619" => "TAIWAN",
|
||||
"7803" => "TANZANIA",
|
||||
"7820" => "TERRITORIO BRITANICO OCEANO INDICO",
|
||||
"7951" => "TIMOR LESTE",
|
||||
"8001" => "TOGO",
|
||||
"8109" => "TONGA",
|
||||
"8052" => "TOQUELAU",
|
||||
"8150" => "TRINIDAD E TOBAGO",
|
||||
"8206" => "TUNISIA",
|
||||
"8230" => "TURCAS E CAICOS",
|
||||
"8249" => "TURCOMENISTAO",
|
||||
"8273" => "TURQUIA",
|
||||
"8281" => "TUVALU",
|
||||
"8311" => "UCRANIA",
|
||||
"8338" => "UGANDA",
|
||||
"8451" => "URUGUAI",
|
||||
"8478" => "UZBEQUISTAO",
|
||||
"5517" => "VANUATU",
|
||||
"8486" => "VATICANO",
|
||||
"8508" => "VENEZUELA",
|
||||
"8583" => "VIETNA",
|
||||
"8630" => "VIRGENS, ILHAS (BRITANICAS)",
|
||||
"8664" => "VIRGENS, ILHAS (EUA)",
|
||||
"8753" => "WALLIS E FUTUNA, ILHAS",
|
||||
"8907" => "ZAMBIA",
|
||||
"6653" => "ZIMBABUE"
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::PAISES;
|
||||
}
|
||||
|
||||
public static function getNomeByCod(string $codigo): ?string {
|
||||
return self::PAISES[$codigo] ?? null;
|
||||
}
|
||||
|
||||
public static function exists(string $codigo): bool {
|
||||
return isset(self::PAISES[$codigo]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Database;
|
||||
|
||||
use WorkbloomERP\Services\DBService;
|
||||
|
||||
class DBFactory {
|
||||
/**
|
||||
* Método para criação simplificada de instâncias do serviço de banco de dados.
|
||||
*
|
||||
* Este método implementa o padrão de projeto Factory, permitindo que o desenvolvedor obtenha
|
||||
* uma instância pronta do **DBService** sem a necessidade de gerenciar manualmente o operador `new`.
|
||||
* Sua principal vantagem é a flexibilidade para alternar entre diferentes conexões e esquemas
|
||||
* (schemas) de banco de dados através de parâmetros opcionais, facilitando a operação em
|
||||
* ambientes multi-banco ou arquiteturas multi-tenant.
|
||||
*
|
||||
*
|
||||
*
|
||||
* ---
|
||||
* ## Funcionalidades Principais
|
||||
* 1. **Abstração de Instanciação:** Centraliza a criação do objeto, permitindo futuras implementações de Singletons ou Service Locators sem quebrar o código cliente.
|
||||
* 2. **Configuração On-the-fly:** Permite definir a conexão (ex: 'mysql_prod', 'sqlite_test') e o schema de trabalho no momento da criação.
|
||||
* 3. **Fluidez:** Ideal para ser utilizado em chamadas encadeadas (ex: `DBService::make()->select(...)`), tornando o código mais legível e conciso.
|
||||
*
|
||||
* ---
|
||||
* ## Exemplos de Uso
|
||||
* - **Conexão Padrão:** `DBService::make();` (Utiliza as definições default da classe).
|
||||
* - **Esquema Específico:** `DBService::make('', 'vendas');` (Mantém a conexão padrão, mas aponta para o schema de vendas).
|
||||
* - **Conexão Externa:** `DBService::make('external_db', 'public');`
|
||||
*
|
||||
* @param string $connection Identificador da conexão configurada no sistema.
|
||||
* @param string $schema Nome do esquema de banco de dados a ser selecionado.
|
||||
* @return DBService Uma nova instância do serviço de banco de dados configurada.
|
||||
*/
|
||||
public static function make(string $connection = 'DEFAULT', string $schema = 'shared'): DBService {
|
||||
return new DBService(connection: $connection, schema: $schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class AppException extends Exception {
|
||||
protected array $details = [];
|
||||
public function __construct(string $message, int $code = 500, array $details = [], ?Throwable $previous = null) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->details = $details;
|
||||
}
|
||||
|
||||
public function getDetails(): array {
|
||||
return $this->details;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Services;
|
||||
|
||||
use KrothiumAPI\Database\RedisManager;
|
||||
|
||||
class CacheService extends RedisManager {
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Services;
|
||||
|
||||
use Throwable;
|
||||
use KrothiumAPI\Database\DBManager;
|
||||
|
||||
class DBService {
|
||||
protected string $connection;
|
||||
protected ?string $schema;
|
||||
|
||||
public function __construct(string $connection = 'DEFAULT', ?string $schema = 'shared') {
|
||||
$this->schema = $schema;
|
||||
$this->connection = strtoupper(string: $connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa dentro de transação
|
||||
*/
|
||||
public function transaction(callable $callback): mixed {
|
||||
DBManager::beginTransaction(connectionName: $this->connection, schema: $this->schema);
|
||||
try {
|
||||
$result = $callback($this);
|
||||
DBManager::commit(connectionName: $this->connection, schema: $this->schema);
|
||||
return $result;
|
||||
} catch (Throwable $e) {
|
||||
DBManager::rollback(connectionName: $this->connection, schema: $this->schema);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa query sem retorno
|
||||
*/
|
||||
public function execute(string $sql, array $params = []): bool {
|
||||
return DBManager::execute(
|
||||
sql: $sql,
|
||||
params: $params,
|
||||
connectionName: $this->connection,
|
||||
schema: $this->schema
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna um único registro
|
||||
*/
|
||||
public function fetchOne(string $sql, array $params = []): ?array {
|
||||
return DBManager::fetchOne(
|
||||
sql: $sql,
|
||||
params: $params,
|
||||
connectionName: $this->connection,
|
||||
schema: $this->schema
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna múltiplos registros
|
||||
*/
|
||||
public function fetchAll(string $sql, array $params = []): array {
|
||||
return DBManager::fetchAll(
|
||||
sql: $sql,
|
||||
params: $params,
|
||||
connectionName: $this->connection,
|
||||
schema: $this->schema
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Último ID inserido (via PDO interno do driver)
|
||||
*/
|
||||
public function lastInsertId(): string {
|
||||
return DBManager::lastInsertId(
|
||||
connectionName: $this->connection,
|
||||
schema: $this->schema
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Utils;
|
||||
|
||||
use WorkbloomERP\Services\CacheService;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
|
||||
class CacheUtil {
|
||||
public static function set(string $key, int $ttl, mixed $value): bool {
|
||||
return CacheService::set(
|
||||
key: $key,
|
||||
value: is_string($value) ? $value : json_encode(
|
||||
value: match (true) {
|
||||
is_array($value), is_object($value) => $value,
|
||||
default => throw new AppException(message: 'Valor para cache deve ser string, array ou objeto.', code: 400)
|
||||
},
|
||||
flags: JSON_UNESCAPED_UNICODE
|
||||
),
|
||||
ttl: max(-1, $ttl)
|
||||
);
|
||||
}
|
||||
|
||||
public static function get(string $key): ?array {
|
||||
$cachedValue = CacheService::get(key: $key);
|
||||
return $cachedValue ? json_decode($cachedValue, true) : null;
|
||||
}
|
||||
|
||||
public static function delete(array $keys): bool {
|
||||
return CacheService::del(keys: $keys);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Utils;
|
||||
|
||||
use Exception;
|
||||
use JsonException;
|
||||
|
||||
class CryptoUtil {
|
||||
/**
|
||||
* Criptografa uma string.
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function encrypt(string $data): string {
|
||||
$algorithm = self::getAlgorithm();
|
||||
$key = self::getKey();
|
||||
|
||||
$ivLength = openssl_cipher_iv_length($algorithm);
|
||||
if ($ivLength === false) {
|
||||
throw new Exception('Unable to determine IV length');
|
||||
}
|
||||
|
||||
$iv = random_bytes($ivLength);
|
||||
$cipherText = openssl_encrypt(
|
||||
$data,
|
||||
$algorithm,
|
||||
$key,
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv,
|
||||
$tag
|
||||
);
|
||||
|
||||
if ($cipherText === false) {
|
||||
throw new Exception('Failed to encrypt data');
|
||||
}
|
||||
|
||||
return self::encodePayload([
|
||||
'iv' => base64_encode($iv),
|
||||
'tag' => base64_encode($tag),
|
||||
'value' => base64_encode($cipherText)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptografa uma string.
|
||||
*
|
||||
* @param string $payload
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function decrypt(string $payload): string {
|
||||
$algorithm = self::getAlgorithm();
|
||||
$key = self::getKey();
|
||||
|
||||
$decodedPayload = self::decodePayload($payload);
|
||||
|
||||
$plainText = openssl_decrypt(
|
||||
base64_decode($decodedPayload['value']),
|
||||
$algorithm,
|
||||
$key,
|
||||
OPENSSL_RAW_DATA,
|
||||
base64_decode($decodedPayload['iv']),
|
||||
base64_decode($decodedPayload['tag'])
|
||||
);
|
||||
if ($plainText === false) {
|
||||
throw new Exception('Failed to decrypt data');
|
||||
}
|
||||
|
||||
return $plainText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o algoritmo configurado.
|
||||
*
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getAlgorithm(): string {
|
||||
$algorithm = $_ENV['SYSTEM_CRYPTO_ALGO'];
|
||||
if (!in_array($algorithm, openssl_get_cipher_methods(), true)) {
|
||||
throw new Exception('Invalid cipher algorithm');
|
||||
}
|
||||
return $algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna a chave de criptografia.
|
||||
*
|
||||
* A chave deve ser uma string hexadecimal de 64 caracteres (32 bytes).
|
||||
*
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getKey(): string {
|
||||
$hexKey = $_ENV['SYSTEM_CRYPTO_KEY'];
|
||||
if (empty($hexKey)) {
|
||||
throw new Exception('Encryption key not configured');
|
||||
}
|
||||
|
||||
$key = hex2bin($hexKey);
|
||||
if ($key === false || strlen($key) !== 32) {
|
||||
throw new Exception(message: 'Invalid encryption key. Expected 32 bytes (64 hex characters).');
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Codifica o payload para armazenamento.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return string
|
||||
* @throws JsonException
|
||||
*/
|
||||
private static function encodePayload(array $payload): string {
|
||||
return base64_encode(
|
||||
json_encode(
|
||||
$payload,
|
||||
JSON_THROW_ON_ERROR
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodifica o payload criptografado.
|
||||
*
|
||||
* @param string $payload
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function decodePayload(string $payload): array {
|
||||
try {
|
||||
$decoded = json_decode(
|
||||
base64_decode($payload),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (JsonException $e) {
|
||||
throw new Exception(message: 'Invalid encrypted payload', previous: $e);
|
||||
}
|
||||
|
||||
foreach (['iv', 'tag', 'value'] as $field) {
|
||||
if (!isset($decoded[$field])) {
|
||||
throw new Exception(sprintf('Missing "%s" field in encrypted payload', $field));
|
||||
}
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Utils;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CypherUtil {
|
||||
public static function hash(string $data): string {
|
||||
if (!isset($_ENV['SYSTEM_CYPHER_ALGO']) || !isset($_ENV['SYSTEM_CYPHER_PEPPER'])) {
|
||||
throw new Exception('Missing required environment variables for hashing');
|
||||
}
|
||||
return hash_hmac(algo: $_ENV['SYSTEM_CYPHER_ALGO'], data: $data, key: $_ENV['SYSTEM_CYPHER_PEPPER']);
|
||||
}
|
||||
|
||||
public static function verify(string $data, string $hash): bool {
|
||||
return hash_equals(known_string: self::hash($data), user_string: $hash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Utils;
|
||||
|
||||
use Exception;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use DateTimeImmutable;
|
||||
|
||||
class JwtUtil {
|
||||
private int $ttl;
|
||||
private string $issuer;
|
||||
private string $secretKey;
|
||||
private string $algorithm;
|
||||
|
||||
public function __construct(string $secretKey, string $algorithm = 'HS256', int $ttl = 3600, string $issuer = 'workbloomerp') {
|
||||
if (empty($secretKey)) {
|
||||
throw new Exception(message: 'JWT secret não configurado.');
|
||||
}
|
||||
$this->ttl = $ttl;
|
||||
$this->secretKey = $secretKey;
|
||||
$this->algorithm = $algorithm;
|
||||
$this->issuer = $issuer;
|
||||
}
|
||||
|
||||
public function generate(array $payload): string {
|
||||
$defaultClaims = [];
|
||||
$defaultClaims['iat'] = $payload['iat'] ?? (new DateTimeImmutable())->getTimestamp();
|
||||
$defaultClaims['exp'] = $payload['exp'] ?? ((new DateTimeImmutable())->modify("+{$this->ttl} seconds")->getTimestamp());
|
||||
|
||||
$finalPayload = array_merge($payload, $defaultClaims);
|
||||
return JWT::encode(
|
||||
payload: $finalPayload,
|
||||
key: $this->secretKey,
|
||||
alg: $this->algorithm
|
||||
);
|
||||
}
|
||||
|
||||
public function validate(string $token): object {
|
||||
return JWT::decode(
|
||||
jwt: $token,
|
||||
keyOrKeyArray: new Key(
|
||||
keyMaterial: $this->secretKey,
|
||||
algorithm: $this->algorithm
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Utils;
|
||||
|
||||
class SanitizeUtil {
|
||||
public static function string(mixed $value): ?string {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
$value = trim(string: $value);
|
||||
$value = strip_tags(string: $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function email(mixed $value): ?string {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
$value = filter_var(value: $value, filter: FILTER_SANITIZE_EMAIL);
|
||||
return $value ?: '';
|
||||
}
|
||||
|
||||
public static function int(mixed $value): ?int {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
return (int) filter_var(value: $value, filter: FILTER_SANITIZE_NUMBER_INT);
|
||||
}
|
||||
|
||||
public static function float(mixed $value): ?float {
|
||||
if($value === null) {
|
||||
return null;
|
||||
}
|
||||
return (float) filter_var(
|
||||
value: $value,
|
||||
filter: FILTER_SANITIZE_NUMBER_FLOAT,
|
||||
options: FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND
|
||||
);
|
||||
}
|
||||
|
||||
public static function document(mixed $value): ?string {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
// remove espaços
|
||||
$value = trim($value);
|
||||
// converte caracteres especiais (Ç -> C, Á -> A, etc.)
|
||||
$value = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
// mantém apenas letras e números
|
||||
$value = preg_replace('/[^a-zA-Z0-9]/', '', $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function boolean(mixed $value): ?bool {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$value = strtolower($value);
|
||||
if (in_array($value, ['true', '1', 'yes'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($value, ['false', '0', 'no'], true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (is_int($value)) {
|
||||
return $value === 1;
|
||||
}
|
||||
// Se não for possível converter, retorna null
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function phone(mixed $value, bool $withCountryCode = false): ?string {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
// remove tudo que não for número
|
||||
$value = preg_replace('/\D/', '', $value);
|
||||
if (!$value) {
|
||||
return '';
|
||||
}
|
||||
// adiciona DDI do Brasil se não tiver
|
||||
if ($withCountryCode) {
|
||||
if (strlen($value) === 10 || strlen($value) === 11) {
|
||||
$value = "55{$value}";
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Utils;
|
||||
|
||||
class ValidateUtil {
|
||||
public static function cpf(?string $cpf): bool {
|
||||
if ($cpf == null) {
|
||||
return false;
|
||||
}
|
||||
// Remove tudo que não for número
|
||||
$cpf = preg_replace('/\D/', '', $cpf);
|
||||
// Precisa ter 11 dígitos
|
||||
if (strlen($cpf) !== 11) {
|
||||
return false;
|
||||
}
|
||||
// Bloqueia CPFs com todos os dígitos iguais (11111111111, 00000000000, etc)
|
||||
if (preg_match('/^(\d)\1{10}$/', $cpf)) {
|
||||
return false;
|
||||
}
|
||||
// Valida primeiro dígito verificador
|
||||
for ($t = 9; $t < 11; $t++) {
|
||||
$soma = 0;
|
||||
for ($i = 0; $i < $t; $i++) {
|
||||
$soma += $cpf[$i] * (($t + 1) - $i);
|
||||
}
|
||||
$digito = ((10 * $soma) % 11) % 10;
|
||||
if ($cpf[$t] != $digito) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function cnpj(?string $cnpj): bool {
|
||||
if ($cnpj == null) {
|
||||
return false;
|
||||
}
|
||||
// Remove máscara
|
||||
$cnpj = strtoupper(preg_replace('/[^A-Z0-9]/', '', $cnpj));
|
||||
// Tamanho fixo
|
||||
if (strlen($cnpj) !== 14) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bloqueia CNPJs com todos os dígitos iguais (11111111111111, 00000000000000, etc)
|
||||
if (preg_match('/^(\d)\1{13}$/', $cnpj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Regex: 12 alfanum + 2 numéricos
|
||||
if (!preg_match('/^[A-Z0-9]{12}[0-9]{2}$/', $cnpj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Converte para valores numéricos
|
||||
$valores = [];
|
||||
for ($i = 0; $i < 14; $i++) {
|
||||
$char = $cnpj[$i];
|
||||
if (ctype_digit($char)) {
|
||||
$valores[$i] = (int)$char;
|
||||
} else {
|
||||
$valores[$i] = ord($char) - 48;
|
||||
}
|
||||
}
|
||||
|
||||
// === Primeiro DV ===
|
||||
$pesos1 = [5,4,3,2,9,8,7,6,5,4,3,2];
|
||||
$soma = 0;
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$soma += $valores[$i] * $pesos1[$i];
|
||||
}
|
||||
|
||||
$resto = $soma % 11;
|
||||
$dv1 = ($resto < 2) ? 0 : 11 - $resto;
|
||||
if ($dv1 !== $valores[12]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// === Segundo DV ===
|
||||
$pesos2 = [6,5,4,3,2,9,8,7,6,5,4,3,2];
|
||||
$soma = 0;
|
||||
for ($i = 0; $i < 13; $i++) {
|
||||
$soma += $valores[$i] * $pesos2[$i];
|
||||
}
|
||||
|
||||
$resto = $soma % 11;
|
||||
$dv2 = ($resto < 2) ? 0 : 11 - $resto;
|
||||
return $dv2 === $valores[13];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "cybercore-systems/workbloom-erp-backend",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"ext-pdo": "*",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-soap": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-bcmath": "*",
|
||||
"claudecio/krothiumapi": "dev-dev/new-http",
|
||||
"vlucas/phpdotenv": "v5.6.2",
|
||||
"ramsey/uuid": "4.9.2",
|
||||
"lcobucci/jwt": "5.6.0",
|
||||
"defuse/php-encryption": "v2.4.0",
|
||||
"symfony/var-dumper": "v7.4.8",
|
||||
"monolog/monolog": "3.10.0",
|
||||
"nesbot/carbon": "3.11.4",
|
||||
"guzzlehttp/guzzle": "7.10.4",
|
||||
"respect/validation": "2.4.12",
|
||||
"league/flysystem": "3.34.0",
|
||||
"league/flysystem-local": "3.31.0",
|
||||
"tecnickcom/tcpdf": "6.11.3",
|
||||
"bacon/bacon-qr-code": "v3.1.1",
|
||||
"firebase/php-jwt": "v6.11.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WorkbloomERP\\": "app/Shared/",
|
||||
"WorkbloomERP\\Module\\": "app/Module/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2985
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
DROP SCHEMA IF EXISTS shared CASCADE;
|
||||
CREATE SCHEMA IF NOT EXISTS shared;
|
||||
CREATE TABLE IF NOT EXISTS shared.usuario (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"is_active" SMALLINT NOT NULL DEFAULT 1,
|
||||
"is_root" SMALLINT NOT NULL DEFAULT 0,
|
||||
"nome_completo" VARCHAR(150) NOT NULL,
|
||||
"nome_usuario" VARCHAR(50) NOT NULL UNIQUE,
|
||||
"email" VARCHAR(255) NOT NULL UNIQUE,
|
||||
"senha_hash" VARCHAR(255) NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP DEFAULT NULL,
|
||||
"deleted_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.empresa (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"is_active" SMALLINT NOT NULL DEFAULT 1,
|
||||
"nome_empresarial" VARCHAR(150) NOT NULL,
|
||||
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
|
||||
"tipo" VARCHAR(50) NOT NULL DEFAULT 'MATRIZ',
|
||||
"matriz_id" INTEGER DEFAULT NULL,
|
||||
"document_cnpj" CHAR(14) NOT NULL UNIQUE,
|
||||
"document_ie" CHAR(14) DEFAULT NULL,
|
||||
"document_im" CHAR(14) DEFAULT NULL,
|
||||
"regime_tributario" CHAR(1) NOT NULL,
|
||||
"end_cep" VARCHAR(8) NOT NULL,
|
||||
"end_ibge" VARCHAR(8) NOT NULL,
|
||||
"end_logradouro" VARCHAR(150) NOT NULL,
|
||||
"end_numero" VARCHAR(20) NOT NULL,
|
||||
"end_complemento" VARCHAR(50) DEFAULT NULL,
|
||||
"end_bairro" VARCHAR(50) NOT NULL,
|
||||
"end_cidade" VARCHAR(150) NOT NULL,
|
||||
"end_uf" CHAR(2) NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP DEFAULT NULL,
|
||||
"deleted_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.usuario_empresa (
|
||||
"usuario_id" INTEGER NOT NULL,
|
||||
"empresa_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY("usuario_id", "empresa_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.usuario_sessao (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"usuario_id" INTEGER NOT NULL,
|
||||
"user_agent" TEXT DEFAULT NULL,
|
||||
"ip_address" VARCHAR(45) DEFAULT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"revoked_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.contato (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"empresa_id" INTEGER NOT NULL,
|
||||
"is_active" SMALLINT NOT NULL DEFAULT 1,
|
||||
"tipo" VARCHAR(50) NOT NULL,
|
||||
"nome_empresarial" VARCHAR(150) NOT NULL,
|
||||
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
|
||||
"personalidade" CHAR(2) NOT NULL DEFAULT 'PJ',
|
||||
"document_cpf" CHAR(11) DEFAULT NULL,
|
||||
"document_cnpj" CHAR(14) DEFAULT NULL,
|
||||
"regime_tributario" CHAR(1) DEFAULT NULL,
|
||||
"contribuinte_icms" CHAR(1) DEFAULT NULL,
|
||||
"orgao_publico" VARCHAR(9) NOT NULL DEFAULT 'NAO',
|
||||
"document_ie" CHAR(14) DEFAULT NULL,
|
||||
"document_im" CHAR(14) DEFAULT NULL,
|
||||
"document_is" CHAR(9) DEFAULT NULL,
|
||||
"end_pais" VARCHAR(255) DEFAULT NULL,
|
||||
"end_cep" VARCHAR(8) NOT NULL,
|
||||
"end_ibge" VARCHAR(8) DEFAULT NULL,
|
||||
"end_logradouro" VARCHAR(150) NOT NULL,
|
||||
"end_numero" VARCHAR(20) NOT NULL,
|
||||
"end_complemento" VARCHAR(50) DEFAULT NULL,
|
||||
"end_bairro" VARCHAR(50) NOT NULL,
|
||||
"end_cidade" VARCHAR(150) NOT NULL,
|
||||
"end_uf" CHAR(2) NOT NULL,
|
||||
"info_email" VARCHAR(255) DEFAULT NULL,
|
||||
"info_email_nfe" VARCHAR(80) DEFAULT NULL,
|
||||
"info_observacao" TEXT DEFAULT NULL,
|
||||
"info_telefone" VARCHAR(11) DEFAULT NULL,
|
||||
"info_uso_consumo_ibs_cbs" CHAR(1) NOT NULL DEFAULT '0',
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP DEFAULT NULL,
|
||||
"deleted_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
ALTER TABLE shared.usuario_sessao ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
ALTER TABLE shared.contato ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Insere um usuário administrador para facilitar o desenvolvimento e testes iniciais do sistema.
|
||||
INSERT INTO shared.usuario (id, uuid, is_active, is_root, nome_completo, nome_usuario, email, senha_hash) VALUES
|
||||
(1, '019ea98d-8876-75c5-a39f-dcb3ad56faad', 1, 1, 'Claudecio Santos da Costa Martins Júnior', 'claudecio.junior', 'claudecio@cybercore.com.br', '$2a$12$13xi5h7mS5NJ/jdG9DarC.SsuMq/3gQoDBwmOjt2xVLLXRhLQ2exC'),
|
||||
(2, '019eb148-946b-7665-99bd-eb6bc5923582', 1, 1, 'Administastror', 'adm', 'adm@cybercore.com.br', '$2a$12$cnblvb71zjc1f3lWIIBsXundwrNOdyiqssft.g.kaNBQAkrNnFhJC');
|
||||
|
||||
SELECT setval(pg_get_serial_sequence('shared.usuario', 'id'), (SELECT MAX(id) FROM shared.usuario));
|
||||
|
||||
-- Insere uma empresa para facilitar o desenvolvimento e testes iniciais do sistema.
|
||||
INSERT INTO shared.empresa (id, uuid, is_active, nome_empresarial, nome_fantasia, tipo, matriz_id, document_cnpj, document_ie, document_im, regime_tributario, end_cep, end_ibge, end_logradouro, end_numero, end_complemento, end_bairro, end_cidade, end_uf) VALUES
|
||||
(1, '019ea998-7fc6-7fba-b433-2836943db898', 1, 'Yuri e Luan Alimentos ME', 'Yuri e Luan Alimentos', 'MATRIZ', NULL, '0KMTA6PD000101', '012303712', '631763333', 1, '60130180', '2304400', 'Travessa Manuel Maia', '966', NULL, 'Joaquim Távora', 'Fortaleza', 'CE'),
|
||||
(2, '019ea998-bc6a-73d7-82a8-63a29aa033be', 1, 'Yuri e Luan Alimentos ME', 'Yuri e Luan Alimentos', 'FILIAL', 1, '0KMTA6PD9ZK308', '632566779', '803316', 1, '60600970', '2303709', 'Rodovia CE-090', 'S/N', 'Km 01', 'Itambém', 'Caucaia', 'CE'),
|
||||
(3, '019ea9a6-69d7-7fc9-b624-2f45b129a07c', 1, 'Antonio e Sueli Marcenaria Ltda', 'Antonio e Sueli Marcenaria', 'MATRIZ', NULL, '53449352000195', '092745032', '023897856', 1, '60346005', '2304400', 'Rua Cecil Salgado', '868', NULL, 'Vila Velha', 'Fortaleza', 'CE'),
|
||||
(4, '019ea9a6-69d7-7fc9-b624-2f45b129a07d', 1, 'Antonio e Sueli Marcenaria Ltda', 'Antonio e Sueli Marcenaria Vila Velha', 'FILIAL', 3, '53449352000276', '092745032', '023897856', 1, '60346005', '2304400', 'Travessa Colinas', '998', NULL, 'Edson Queiroz', 'Fortaleza', 'CE');
|
||||
|
||||
SELECT setval(pg_get_serial_sequence('shared.empresa', 'id'), (SELECT MAX(id) FROM shared.empresa));
|
||||
|
||||
-- Associa o usuário administrador à empresa criada.
|
||||
INSERT INTO shared.usuario_empresa (usuario_id, empresa_id) VALUES
|
||||
(1, 1),
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(1, 4);
|
||||
@@ -0,0 +1,99 @@
|
||||
CREATE TABLE IF NOT EXISTS shared.usuario (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"is_active" SMALLINT NOT NULL DEFAULT 1,
|
||||
"is_root" SMALLINT NOT NULL DEFAULT 0,
|
||||
"nome_completo" VARCHAR(150) NOT NULL,
|
||||
"nome_usuario" VARCHAR(50) NOT NULL UNIQUE,
|
||||
"email" VARCHAR(255) NOT NULL UNIQUE,
|
||||
"senha_hash" VARCHAR(255) NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP DEFAULT NULL,
|
||||
"deleted_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.empresa (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"is_active" SMALLINT NOT NULL DEFAULT 1,
|
||||
"nome_empresarial" VARCHAR(150) NOT NULL,
|
||||
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
|
||||
"tipo" VARCHAR(50) NOT NULL DEFAULT 'MATRIZ',
|
||||
"matriz_id" INTEGER DEFAULT NULL,
|
||||
"document_cnpj" CHAR(14) NOT NULL UNIQUE,
|
||||
"document_ie" CHAR(14) DEFAULT NULL,
|
||||
"document_im" CHAR(14) DEFAULT NULL,
|
||||
"regime_tributario" CHAR(1) NOT NULL,
|
||||
"end_cep" VARCHAR(8) NOT NULL,
|
||||
"end_ibge" VARCHAR(8) NOT NULL,
|
||||
"end_logradouro" VARCHAR(150) NOT NULL,
|
||||
"end_numero" VARCHAR(20) NOT NULL,
|
||||
"end_complemento" VARCHAR(50) DEFAULT NULL,
|
||||
"end_bairro" VARCHAR(50) NOT NULL,
|
||||
"end_cidade" VARCHAR(150) NOT NULL,
|
||||
"end_uf" CHAR(2) NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP DEFAULT NULL,
|
||||
"deleted_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.usuario_empresa (
|
||||
"usuario_id" INTEGER NOT NULL,
|
||||
"empresa_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY("usuario_id", "empresa_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.usuario_sessao (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"usuario_id" INTEGER NOT NULL,
|
||||
"user_agent" TEXT DEFAULT NULL,
|
||||
"ip_address" VARCHAR(45) DEFAULT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"revoked_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shared.contato (
|
||||
"id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"uuid" UUID NOT NULL UNIQUE,
|
||||
"empresa_id" INTEGER NOT NULL,
|
||||
"is_active" SMALLINT NOT NULL DEFAULT 1,
|
||||
"tipo" VARCHAR(50) NOT NULL,
|
||||
"nome_empresarial" VARCHAR(150) NOT NULL,
|
||||
"nome_fantasia" VARCHAR(60) DEFAULT NULL,
|
||||
"personalidade" CHAR(2) NOT NULL DEFAULT 'PJ',
|
||||
"document_cpf" CHAR(11) DEFAULT NULL,
|
||||
"document_cnpj" CHAR(14) DEFAULT NULL,
|
||||
"regime_tributario" CHAR(1) DEFAULT NULL,
|
||||
"contribuinte_icms" CHAR(1) DEFAULT NULL,
|
||||
"orgao_publico" VARCHAR(9) NOT NULL DEFAULT 'NAO',
|
||||
"document_ie" CHAR(14) DEFAULT NULL,
|
||||
"document_im" CHAR(14) DEFAULT NULL,
|
||||
"document_is" CHAR(9) DEFAULT NULL,
|
||||
"end_pais" VARCHAR(255) DEFAULT NULL,
|
||||
"end_cep" VARCHAR(8) NOT NULL,
|
||||
"end_ibge" VARCHAR(8) DEFAULT NULL,
|
||||
"end_logradouro" VARCHAR(150) NOT NULL,
|
||||
"end_numero" VARCHAR(20) NOT NULL,
|
||||
"end_complemento" VARCHAR(50) DEFAULT NULL,
|
||||
"end_bairro" VARCHAR(50) NOT NULL,
|
||||
"end_cidade" VARCHAR(150) NOT NULL,
|
||||
"end_uf" CHAR(2) NOT NULL,
|
||||
"info_email" VARCHAR(255) DEFAULT NULL,
|
||||
"info_email_nfe" VARCHAR(80) DEFAULT NULL,
|
||||
"info_observacao" TEXT DEFAULT NULL,
|
||||
"info_telefone" VARCHAR(11) DEFAULT NULL,
|
||||
"info_uso_consumo_ibs_cbs" CHAR(1) NOT NULL DEFAULT '0',
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP DEFAULT NULL,
|
||||
"deleted_at" TIMESTAMP DEFAULT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);
|
||||
|
||||
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
ALTER TABLE shared.usuario_empresa ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
ALTER TABLE shared.usuario_sessao ADD FOREIGN KEY("usuario_id") REFERENCES shared.usuario("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
ALTER TABLE shared.contato ADD FOREIGN KEY("empresa_id") REFERENCES shared.empresa("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
# Se for arquivo ou pasta real, deixa passar
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
# Tudo vai pro controller
|
||||
RewriteRule ^ index.php [L,QSA]
|
||||
</IfModule>
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
// Importa autoload do Composer
|
||||
require_once realpath(path: __DIR__ . '/../vendor/autoload.php');
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use KrothiumAPI\KrothiumAPI;
|
||||
use KrothiumAPI\Http\Router;
|
||||
|
||||
// Carrega variáveis de ambiente
|
||||
$dotenv = Dotenv::createImmutable(paths: realpath(path: __DIR__ . '/../'));
|
||||
$dotenv->load();
|
||||
|
||||
// ======================================
|
||||
// Inicializa KrothiumAPI com configs
|
||||
// ======================================
|
||||
KrothiumAPI::init(config: [
|
||||
'errors' => [
|
||||
'error_log' => realpath(path: __DIR__ . '/../storage/Logs/php-error.log'),
|
||||
],
|
||||
'constants' => [
|
||||
'APP_SYS_MODE' => 'DEV', // DEV | PROD
|
||||
'ROOT_SYSTEM_PATH' => realpath(path: __DIR__ . "/.."),
|
||||
'INI_SYSTEM_PATH' => realpath(path: __DIR__ . "/../src"),
|
||||
'MODULE_PATH' => realpath(path: __DIR__ . "/../src/Module"),
|
||||
'STORAGE_FOLDER_PATH' => realpath(path: __DIR__ . "/../storage"),
|
||||
'ROUTER_ALLOWED_ORIGINS' => [
|
||||
'*'
|
||||
]
|
||||
],
|
||||
'system' => [
|
||||
'enable_session' => true,
|
||||
'default_timezone' => 'America/Fortaleza',
|
||||
],
|
||||
'logger' => [
|
||||
'driver' => 'FILE',
|
||||
'logDir' => realpath(path: __DIR__ . '/../storage/Logs')
|
||||
]
|
||||
]);
|
||||
|
||||
// Importa Rotas da v0
|
||||
Router::group(
|
||||
prefix: '/v0',
|
||||
callback: function() {
|
||||
require_once realpath(path: __DIR__ . '/../app/Module/v0/Auth/Routes/Routes.php');
|
||||
require_once realpath(path: __DIR__ . '/../app/Module/v0/Contato/Routes/Routes.php');
|
||||
}
|
||||
);
|
||||
|
||||
// ============================
|
||||
// Dispara o roteador
|
||||
// ============================
|
||||
KrothiumAPI::routerDispatch();
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInitcdaa22da496807f150a36aa282af66ef::getLoader();
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2017-present, Ben Scholzen 'DASPRiD'
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
# QR Code generator
|
||||
|
||||
[](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/Bacon/BaconQrCode)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
|
||||
|
||||
## Introduction
|
||||
BaconQrCode is a port of QR code portion of the ZXing library. It currently
|
||||
only features the encoder part, but could later receive the decoder part as
|
||||
well.
|
||||
|
||||
As the Reed Solomon codec implementation of the ZXing library performs quite
|
||||
slow in PHP, it was exchanged with the implementation by Phil Karn.
|
||||
|
||||
|
||||
## Example usage
|
||||
```php
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
$renderer = new ImageRenderer(
|
||||
new RendererStyle(400),
|
||||
new ImagickImageBackEnd()
|
||||
);
|
||||
$writer = new Writer($renderer);
|
||||
$writer->writeFile('Hello World!', 'qrcode.png');
|
||||
```
|
||||
|
||||
## Available image renderer back ends
|
||||
BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
|
||||
|
||||
- `ImagickImageBackEnd`: renders raster images using the Imagick library
|
||||
- `SvgImageBackEnd`: renders SVG files using XMLWriter
|
||||
- `EpsImageBackEnd`: renders EPS files
|
||||
|
||||
### GDLib Renderer
|
||||
GD library has so many limitations, that GD support is not added as backend, but as separated renderer.
|
||||
Use `GDLibRenderer` instead of `ImageRenderer`. These are the limitations:
|
||||
|
||||
- Does not support gradient.
|
||||
- Does not support any curves, so you QR code is always squared.
|
||||
|
||||
Example usage:
|
||||
|
||||
```php
|
||||
use BaconQrCode\Renderer\GDLibRenderer;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
$renderer = new GDLibRenderer(400);
|
||||
$writer = new Writer($renderer);
|
||||
$writer->writeFile('Hello World!', 'qrcode.png');
|
||||
```
|
||||
|
||||
## Known issues
|
||||
|
||||
### ImagickImageBackEnd: white pixel artifacts
|
||||
|
||||
When using `ImagickImageBackEnd`, single white pixels may appear inside filled regions. This is
|
||||
most visible with margin 0 (where artifacts appear at the image edge), but can in theory occur at
|
||||
any position. The cause is a bug in ImageMagick's path fill rasterizer (`GetFillAlpha` in
|
||||
`MagickCore/draw.c`): an off-by-one error in the winding number calculation combined with an edge
|
||||
skipping bug in the scanline processing can incorrectly classify pixels as outside the polygon.
|
||||
|
||||
The bug cannot be reliably worked around in this library:
|
||||
|
||||
- **Canvas padding** (rendering on a larger canvas and cropping) does not work because the required
|
||||
padding depends on the scale factor, path complexity, and ImageMagick's internal edge processing
|
||||
state. No fixed padding value is safe for all inputs.
|
||||
- **Post-processing** (scanning for and fixing isolated white pixels) risks corrupting legitimate
|
||||
rendering features such as curved module edges.
|
||||
|
||||
For artifact-free output, use `SvgImageBackEnd` or `GDLibRenderer` instead.
|
||||
|
||||
## Development
|
||||
|
||||
To run unit tests, you need to have [Node.js](https://nodejs.org/en) and the pixelmatch library installed. Running
|
||||
`npm install` will install this for you.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||
"license": "BSD-2-Clause",
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-iconv": "*",
|
||||
"dasprid/enum": "^1.0.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BaconQrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"BaconQrCodeTest\\": "test/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5.11 || ^11.0.4",
|
||||
"spatie/phpunit-snapshot-assertions": "^5.1.5",
|
||||
"spatie/pixelmatch-php": "^1.2.0",
|
||||
"squizlabs/php_codesniffer": "^3.9",
|
||||
"phly/keep-a-changelog": "^2.12"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"ocramius/package-versions": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"exclude": [
|
||||
"/test",
|
||||
"/phpunit.xml.dist"
|
||||
]
|
||||
}
|
||||
}
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* A simple, fast array of bits.
|
||||
*/
|
||||
final class BitArray
|
||||
{
|
||||
/**
|
||||
* Bits represented as an array of integers.
|
||||
*
|
||||
* @var SplFixedArray<int>
|
||||
*/
|
||||
private SplFixedArray $bits;
|
||||
|
||||
/**
|
||||
* Creates a new bit array with a given size.
|
||||
*/
|
||||
public function __construct(private int $size = 0)
|
||||
{
|
||||
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size in bits.
|
||||
*/
|
||||
public function getSize() : int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size in bytes.
|
||||
*/
|
||||
public function getSizeInBytes() : int
|
||||
{
|
||||
return ($this->size + 7) >> 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the array has a minimum capacity.
|
||||
*/
|
||||
public function ensureCapacity(int $size) : void
|
||||
{
|
||||
if ($size > count($this->bits) << 5) {
|
||||
$this->bits->setSize(($size + 31) >> 5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific bit.
|
||||
*/
|
||||
public function get(int $i) : bool
|
||||
{
|
||||
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific bit.
|
||||
*/
|
||||
public function set(int $i) : void
|
||||
{
|
||||
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips a specific bit.
|
||||
*/
|
||||
public function flip(int $i) : void
|
||||
{
|
||||
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next set bit position from a given position.
|
||||
*/
|
||||
public function getNextSet(int $from) : int
|
||||
{
|
||||
if ($from >= $this->size) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$bitsOffset = $from >> 5;
|
||||
$currentBits = $this->bits[$bitsOffset];
|
||||
$bitsLength = count($this->bits);
|
||||
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||
|
||||
while (0 === $currentBits) {
|
||||
if (++$bitsOffset === $bitsLength) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$currentBits = $this->bits[$bitsOffset];
|
||||
}
|
||||
|
||||
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||
return min($result, $this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next unset bit position from a given position.
|
||||
*/
|
||||
public function getNextUnset(int $from) : int
|
||||
{
|
||||
if ($from >= $this->size) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$bitsOffset = $from >> 5;
|
||||
$currentBits = ~$this->bits[$bitsOffset];
|
||||
$bitsLength = count($this->bits);
|
||||
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||
|
||||
while (0 === $currentBits) {
|
||||
if (++$bitsOffset === $bitsLength) {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
$currentBits = ~$this->bits[$bitsOffset];
|
||||
}
|
||||
|
||||
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||
return min($result, $this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a bulk of bits.
|
||||
*/
|
||||
public function setBulk(int $i, int $newBits) : void
|
||||
{
|
||||
$this->bits[$i >> 5] = $newBits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a range of bits.
|
||||
*
|
||||
* @throws InvalidArgumentException if end is smaller than start
|
||||
*/
|
||||
public function setRange(int $start, int $end) : void
|
||||
{
|
||||
if ($end < $start) {
|
||||
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||
}
|
||||
|
||||
if ($end === $start) {
|
||||
return;
|
||||
}
|
||||
|
||||
--$end;
|
||||
|
||||
$firstInt = $start >> 5;
|
||||
$lastInt = $end >> 5;
|
||||
|
||||
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||
|
||||
if (0 === $firstBit && 31 === $lastBit) {
|
||||
$mask = 0x7fffffff;
|
||||
} else {
|
||||
$mask = 0;
|
||||
|
||||
for ($j = $firstBit; $j < $lastBit; ++$j) {
|
||||
$mask |= 1 << $j;
|
||||
}
|
||||
}
|
||||
|
||||
$this->bits[$i] = $this->bits[$i] | $mask;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the bit array, unsetting every bit.
|
||||
*/
|
||||
public function clear() : void
|
||||
{
|
||||
$bitsLength = count($this->bits);
|
||||
|
||||
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||
$this->bits[$i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a range of bits is set or not set.
|
||||
|
||||
* @throws InvalidArgumentException if end is smaller than start
|
||||
*/
|
||||
public function isRange(int $start, int $end, bool $value) : bool
|
||||
{
|
||||
if ($end < $start) {
|
||||
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||
}
|
||||
|
||||
if ($end === $start) {
|
||||
return true;
|
||||
}
|
||||
|
||||
--$end;
|
||||
|
||||
$firstInt = $start >> 5;
|
||||
$lastInt = $end >> 5;
|
||||
|
||||
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||
|
||||
if (0 === $firstBit && 31 === $lastBit) {
|
||||
$mask = 0x7fffffff;
|
||||
} else {
|
||||
$mask = 0;
|
||||
|
||||
for ($j = $firstBit; $j <= $lastBit; ++$j) {
|
||||
$mask |= 1 << $j;
|
||||
}
|
||||
}
|
||||
|
||||
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a bit to the array.
|
||||
*/
|
||||
public function appendBit(bool $bit) : void
|
||||
{
|
||||
$this->ensureCapacity($this->size + 1);
|
||||
|
||||
if ($bit) {
|
||||
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
|
||||
}
|
||||
|
||||
++$this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a number of bits (up to 32) to the array.
|
||||
|
||||
* @throws InvalidArgumentException if num bits is not between 0 and 32
|
||||
*/
|
||||
public function appendBits(int $value, int $numBits) : void
|
||||
{
|
||||
if ($numBits < 0 || $numBits > 32) {
|
||||
throw new InvalidArgumentException('Num bits must be between 0 and 32');
|
||||
}
|
||||
|
||||
$this->ensureCapacity($this->size + $numBits);
|
||||
|
||||
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
|
||||
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends another bit array to this array.
|
||||
*/
|
||||
public function appendBitArray(self $other) : void
|
||||
{
|
||||
$otherSize = $other->getSize();
|
||||
$this->ensureCapacity($this->size + $other->getSize());
|
||||
|
||||
for ($i = 0; $i < $otherSize; ++$i) {
|
||||
$this->appendBit($other->get($i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an exclusive-or comparision on the current bit array.
|
||||
*
|
||||
* @throws InvalidArgumentException if sizes don't match
|
||||
*/
|
||||
public function xorBits(self $other) : void
|
||||
{
|
||||
$bitsLength = count($this->bits);
|
||||
$otherBits = $other->getBitArray();
|
||||
|
||||
if ($bitsLength !== count($otherBits)) {
|
||||
throw new InvalidArgumentException('Sizes don\'t match');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the bit array to a byte array.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
|
||||
{
|
||||
$bytes = new SplFixedArray($numBytes);
|
||||
|
||||
for ($i = 0; $i < $numBytes; ++$i) {
|
||||
$byte = 0;
|
||||
|
||||
for ($j = 0; $j < 8; ++$j) {
|
||||
if ($this->get($bitOffset)) {
|
||||
$byte |= 1 << (7 - $j);
|
||||
}
|
||||
|
||||
++$bitOffset;
|
||||
}
|
||||
|
||||
$bytes[$i] = $byte;
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal bit array.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function getBitArray() : SplFixedArray
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the array.
|
||||
*/
|
||||
public function reverse() : void
|
||||
{
|
||||
$newBits = new SplFixedArray(count($this->bits));
|
||||
|
||||
for ($i = 0; $i < $this->size; ++$i) {
|
||||
if ($this->get($this->size - $i - 1)) {
|
||||
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
|
||||
}
|
||||
}
|
||||
|
||||
$this->bits = $newBits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the bit array.
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
for ($i = 0; $i < $this->size; ++$i) {
|
||||
if (0 === ($i & 0x07)) {
|
||||
$result .= ' ';
|
||||
}
|
||||
|
||||
$result .= $this->get($i) ? 'X' : '.';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Bit matrix.
|
||||
*
|
||||
* Represents a 2D matrix of bits. In function arguments below, and throughout
|
||||
* the common module, x is the column position, and y is the row position. The
|
||||
* ordering is always x, y. The origin is at the top-left.
|
||||
*/
|
||||
class BitMatrix
|
||||
{
|
||||
/**
|
||||
* Width of the bit matrix.
|
||||
*/
|
||||
private int $width;
|
||||
|
||||
/**
|
||||
* Height of the bit matrix.
|
||||
*/
|
||||
private ?int $height;
|
||||
|
||||
/**
|
||||
* Size in bits of each individual row.
|
||||
*/
|
||||
private int $rowSize;
|
||||
|
||||
/**
|
||||
* Bits representation.
|
||||
*
|
||||
* @var SplFixedArray<int>
|
||||
*/
|
||||
private SplFixedArray $bits;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if a dimension is smaller than zero
|
||||
*/
|
||||
public function __construct(int $width, ?int $height = null)
|
||||
{
|
||||
if (null === $height) {
|
||||
$height = $width;
|
||||
}
|
||||
|
||||
if ($width < 1 || $height < 1) {
|
||||
throw new InvalidArgumentException('Both dimensions must be greater than zero');
|
||||
}
|
||||
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
$this->rowSize = ($width + 31) >> 5;
|
||||
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the requested bit, where true means black.
|
||||
*/
|
||||
public function get(int $x, int $y) : bool
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given bit to true.
|
||||
*/
|
||||
public function set(int $x, int $y) : void
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips the given bit.
|
||||
*/
|
||||
public function flip(int $x, int $y) : void
|
||||
{
|
||||
$offset = $y * $this->rowSize + ($x >> 5);
|
||||
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all bits (set to false).
|
||||
*/
|
||||
public function clear() : void
|
||||
{
|
||||
$max = count($this->bits);
|
||||
|
||||
for ($i = 0; $i < $max; ++$i) {
|
||||
$this->bits[$i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a square region of the bit matrix to true.
|
||||
*
|
||||
* @throws InvalidArgumentException if left or top are negative
|
||||
* @throws InvalidArgumentException if width or height are smaller than 1
|
||||
* @throws InvalidArgumentException if region does not fit into the matix
|
||||
*/
|
||||
public function setRegion(int $left, int $top, int $width, int $height) : void
|
||||
{
|
||||
if ($top < 0 || $left < 0) {
|
||||
throw new InvalidArgumentException('Left and top must be non-negative');
|
||||
}
|
||||
|
||||
if ($height < 1 || $width < 1) {
|
||||
throw new InvalidArgumentException('Width and height must be at least 1');
|
||||
}
|
||||
|
||||
$right = $left + $width;
|
||||
$bottom = $top + $height;
|
||||
|
||||
if ($bottom > $this->height || $right > $this->width) {
|
||||
throw new InvalidArgumentException('The region must fit inside the matrix');
|
||||
}
|
||||
|
||||
for ($y = $top; $y < $bottom; ++$y) {
|
||||
$offset = $y * $this->rowSize;
|
||||
|
||||
for ($x = $left; $x < $right; ++$x) {
|
||||
$index = $offset + ($x >> 5);
|
||||
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A fast method to retrieve one row of data from the matrix as a BitArray.
|
||||
*/
|
||||
public function getRow(int $y, ?BitArray $row = null) : BitArray
|
||||
{
|
||||
if (null === $row || $row->getSize() < $this->width) {
|
||||
$row = new BitArray($this->width);
|
||||
}
|
||||
|
||||
$offset = $y * $this->rowSize;
|
||||
|
||||
for ($x = 0; $x < $this->rowSize; ++$x) {
|
||||
$row->setBulk($x << 5, $this->bits[$offset + $x]);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a row of data from a BitArray.
|
||||
*/
|
||||
public function setRow(int $y, BitArray $row) : void
|
||||
{
|
||||
$bits = $row->getBitArray();
|
||||
|
||||
for ($i = 0; $i < $this->rowSize; ++$i) {
|
||||
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getEnclosingRectangle() : ?array
|
||||
{
|
||||
$left = $this->width;
|
||||
$top = $this->height;
|
||||
$right = -1;
|
||||
$bottom = -1;
|
||||
|
||||
for ($y = 0; $y < $this->height; ++$y) {
|
||||
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
|
||||
$bits = $this->bits[$y * $this->rowSize + $x32];
|
||||
|
||||
if (0 !== $bits) {
|
||||
if ($y < $top) {
|
||||
$top = $y;
|
||||
}
|
||||
|
||||
if ($y > $bottom) {
|
||||
$bottom = $y;
|
||||
}
|
||||
|
||||
if ($x32 * 32 < $left) {
|
||||
$bit = 0;
|
||||
|
||||
while (($bits << (31 - $bit)) === 0) {
|
||||
$bit++;
|
||||
}
|
||||
|
||||
if (($x32 * 32 + $bit) < $left) {
|
||||
$left = $x32 * 32 + $bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($x32 * 32 + 31 > $right) {
|
||||
$bit = 31;
|
||||
|
||||
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||
--$bit;
|
||||
}
|
||||
|
||||
if (($x32 * 32 + $bit) > $right) {
|
||||
$right = $x32 * 32 + $bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$width = $right - $left;
|
||||
$height = $bottom - $top;
|
||||
|
||||
if ($width < 0 || $height < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$left, $top, $width, $height];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most top left set bit.
|
||||
*
|
||||
* This is useful in detecting a corner of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getTopLeftOnBit() : ?array
|
||||
{
|
||||
$bitsOffset = 0;
|
||||
|
||||
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
|
||||
++$bitsOffset;
|
||||
}
|
||||
|
||||
if (count($this->bits) === $bitsOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = intdiv($bitsOffset, $this->rowSize);
|
||||
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||
|
||||
$bits = $this->bits[$bitsOffset];
|
||||
$bit = 0;
|
||||
|
||||
while (0 === ($bits << (31 - $bit))) {
|
||||
++$bit;
|
||||
}
|
||||
|
||||
$x += $bit;
|
||||
|
||||
return [$x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most bottom right set bit.
|
||||
*
|
||||
* This is useful in detecting a corner of a 'pure' barcode.
|
||||
*
|
||||
* @return int[]|null
|
||||
*/
|
||||
public function getBottomRightOnBit() : ?array
|
||||
{
|
||||
$bitsOffset = count($this->bits) - 1;
|
||||
|
||||
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
|
||||
--$bitsOffset;
|
||||
}
|
||||
|
||||
if ($bitsOffset < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$x = intdiv($bitsOffset, $this->rowSize);
|
||||
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||
|
||||
$bits = $this->bits[$bitsOffset];
|
||||
$bit = 0;
|
||||
|
||||
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||
--$bit;
|
||||
}
|
||||
|
||||
$x += $bit;
|
||||
|
||||
return [$x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the matrix,
|
||||
*/
|
||||
public function getWidth() : int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the matrix.
|
||||
*/
|
||||
public function getHeight() : int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* General bit utilities.
|
||||
*
|
||||
* All utility methods are based on 32-bit integers and also work on 64-bit
|
||||
* systems.
|
||||
*/
|
||||
final class BitUtils
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an unsigned right shift.
|
||||
*
|
||||
* This is the same as the unsigned right shift operator ">>>" in other
|
||||
* languages.
|
||||
*/
|
||||
public static function unsignedRightShift(int $a, int $b) : int
|
||||
{
|
||||
return (
|
||||
$a >= 0
|
||||
? $a >> $b
|
||||
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of trailing zeros.
|
||||
*/
|
||||
public static function numberOfTrailingZeros(int $i) : int
|
||||
{
|
||||
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
|
||||
return $lastPos === false ? 32 : 31 - $lastPos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use DASPRiD\Enum\AbstractEnum;
|
||||
|
||||
/**
|
||||
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
|
||||
*
|
||||
* @method static self CP437()
|
||||
* @method static self ISO8859_1()
|
||||
* @method static self ISO8859_2()
|
||||
* @method static self ISO8859_3()
|
||||
* @method static self ISO8859_4()
|
||||
* @method static self ISO8859_5()
|
||||
* @method static self ISO8859_6()
|
||||
* @method static self ISO8859_7()
|
||||
* @method static self ISO8859_8()
|
||||
* @method static self ISO8859_9()
|
||||
* @method static self ISO8859_10()
|
||||
* @method static self ISO8859_11()
|
||||
* @method static self ISO8859_12()
|
||||
* @method static self ISO8859_13()
|
||||
* @method static self ISO8859_14()
|
||||
* @method static self ISO8859_15()
|
||||
* @method static self ISO8859_16()
|
||||
* @method static self SJIS()
|
||||
* @method static self CP1250()
|
||||
* @method static self CP1251()
|
||||
* @method static self CP1252()
|
||||
* @method static self CP1256()
|
||||
* @method static self UNICODE_BIG_UNMARKED()
|
||||
* @method static self UTF8()
|
||||
* @method static self ASCII()
|
||||
* @method static self BIG5()
|
||||
* @method static self GB18030()
|
||||
* @method static self EUC_KR()
|
||||
*/
|
||||
final class CharacterSetEci extends AbstractEnum
|
||||
{
|
||||
protected const CP437 = [[0, 2]];
|
||||
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
|
||||
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
|
||||
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
|
||||
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
|
||||
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
|
||||
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
|
||||
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
|
||||
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
|
||||
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
|
||||
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
|
||||
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
|
||||
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
|
||||
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
|
||||
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
|
||||
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
|
||||
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
|
||||
protected const SJIS = [[20], 'Shift_JIS'];
|
||||
protected const CP1250 = [[21], 'windows-1250'];
|
||||
protected const CP1251 = [[22], 'windows-1251'];
|
||||
protected const CP1252 = [[23], 'windows-1252'];
|
||||
protected const CP1256 = [[24], 'windows-1256'];
|
||||
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
|
||||
protected const UTF8 = [[26], 'UTF-8'];
|
||||
protected const ASCII = [[27, 170], 'US-ASCII'];
|
||||
protected const BIG5 = [[28]];
|
||||
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
|
||||
protected const EUC_KR = [[30], 'EUC-KR'];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $otherEncodingNames;
|
||||
|
||||
/**
|
||||
* @var array<int, self>|null
|
||||
*/
|
||||
private static ?array $valueToEci;
|
||||
|
||||
/**
|
||||
* @var array<string, self>|null
|
||||
*/
|
||||
private static ?array $nameToEci = null;
|
||||
|
||||
/**
|
||||
* @param int[] $values
|
||||
*/
|
||||
public function __construct(private readonly array $values, string ...$otherEncodingNames)
|
||||
{
|
||||
$this->otherEncodingNames = $otherEncodingNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary value.
|
||||
*/
|
||||
public function getValue() : int
|
||||
{
|
||||
return $this->values[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets character set ECI by value.
|
||||
*
|
||||
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
|
||||
*
|
||||
* @throws InvalidArgumentException if value is not between 0 and 900
|
||||
*/
|
||||
public static function getCharacterSetEciByValue(int $value) : ?self
|
||||
{
|
||||
if ($value < 0 || $value >= 900) {
|
||||
throw new InvalidArgumentException('Value must be between 0 and 900');
|
||||
}
|
||||
|
||||
$valueToEci = self::valueToEci();
|
||||
|
||||
if (! array_key_exists($value, $valueToEci)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $valueToEci[$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns character set ECI by name.
|
||||
*
|
||||
* Returns the representing ECI of a given name, or null if it is legal but unsupported
|
||||
*/
|
||||
public static function getCharacterSetEciByName(string $name) : ?self
|
||||
{
|
||||
$nameToEci = self::nameToEci();
|
||||
$name = strtolower($name);
|
||||
|
||||
if (! array_key_exists($name, $nameToEci)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $nameToEci[$name];
|
||||
}
|
||||
|
||||
private static function valueToEci() : array
|
||||
{
|
||||
if (null !== self::$valueToEci) {
|
||||
return self::$valueToEci;
|
||||
}
|
||||
|
||||
self::$valueToEci = [];
|
||||
|
||||
foreach (self::values() as $eci) {
|
||||
foreach ($eci->values as $value) {
|
||||
self::$valueToEci[$value] = $eci;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$valueToEci;
|
||||
}
|
||||
|
||||
private static function nameToEci() : array
|
||||
{
|
||||
if (null !== self::$nameToEci) {
|
||||
return self::$nameToEci;
|
||||
}
|
||||
|
||||
self::$nameToEci = [];
|
||||
|
||||
foreach (self::values() as $eci) {
|
||||
self::$nameToEci[strtolower($eci->name())] = $eci;
|
||||
|
||||
foreach ($eci->otherEncodingNames as $name) {
|
||||
self::$nameToEci[strtolower($name)] = $eci;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$nameToEci;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* Encapsulates the parameters for one error-correction block in one symbol version.
|
||||
*
|
||||
* This includes the number of data codewords, and the number of times a block with these parameters is used
|
||||
* consecutively in the QR code version's format.
|
||||
*/
|
||||
final class EcBlock
|
||||
{
|
||||
public function __construct(private readonly int $count, private readonly int $dataCodewords)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many times the block is used.
|
||||
*/
|
||||
public function getCount() : int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of data codewords.
|
||||
*/
|
||||
public function getDataCodewords() : int
|
||||
{
|
||||
return $this->dataCodewords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* Encapsulates a set of error-correction blocks in one symbol version.
|
||||
*
|
||||
* Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each
|
||||
* set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all
|
||||
* blocks within one version.
|
||||
*/
|
||||
final class EcBlocks
|
||||
{
|
||||
/**
|
||||
* List of EC blocks.
|
||||
*
|
||||
* @var EcBlock[]
|
||||
*/
|
||||
private array $ecBlocks;
|
||||
|
||||
public function __construct(private readonly int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
|
||||
{
|
||||
$this->ecBlocks = $ecBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of EC codewords per block.
|
||||
*/
|
||||
public function getEcCodewordsPerBlock() : int
|
||||
{
|
||||
return $this->ecCodewordsPerBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of EC block appearances.
|
||||
*/
|
||||
public function getNumBlocks() : int
|
||||
{
|
||||
$total = 0;
|
||||
|
||||
foreach ($this->ecBlocks as $ecBlock) {
|
||||
$total += $ecBlock->getCount();
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total count of EC codewords.
|
||||
*/
|
||||
public function getTotalEcCodewords() : int
|
||||
{
|
||||
return $this->ecCodewordsPerBlock * $this->getNumBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the EC blocks included in this collection.
|
||||
*
|
||||
* @return EcBlock[]
|
||||
*/
|
||||
public function getEcBlocks() : array
|
||||
{
|
||||
return $this->ecBlocks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\OutOfBoundsException;
|
||||
use DASPRiD\Enum\AbstractEnum;
|
||||
|
||||
/**
|
||||
* Enum representing the four error correction levels.
|
||||
*
|
||||
* @method static self L() ~7% correction
|
||||
* @method static self M() ~15% correction
|
||||
* @method static self Q() ~25% correction
|
||||
* @method static self H() ~30% correction
|
||||
*/
|
||||
final class ErrorCorrectionLevel extends AbstractEnum
|
||||
{
|
||||
protected const L = [0x01];
|
||||
protected const M = [0x00];
|
||||
protected const Q = [0x03];
|
||||
protected const H = [0x02];
|
||||
|
||||
protected function __construct(private readonly int $bits)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OutOfBoundsException if number of bits is invalid
|
||||
*/
|
||||
public static function forBits(int $bits) : self
|
||||
{
|
||||
switch ($bits) {
|
||||
case 0:
|
||||
return self::M();
|
||||
|
||||
case 1:
|
||||
return self::L();
|
||||
|
||||
case 2:
|
||||
return self::H();
|
||||
|
||||
case 3:
|
||||
return self::Q();
|
||||
}
|
||||
|
||||
throw new OutOfBoundsException('Invalid number of bits');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the two bits used to encode this error correction level.
|
||||
*/
|
||||
public function getBits() : int
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* BaconQrCode
|
||||
*
|
||||
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
|
||||
* @copyright 2013 Ben 'DASPRiD' Scholzen
|
||||
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
|
||||
*/
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
/**
|
||||
* Encapsulates a QR Code's format information, including the data mask used and error correction level.
|
||||
*/
|
||||
class FormatInformation
|
||||
{
|
||||
/**
|
||||
* Mask for format information.
|
||||
*/
|
||||
private const FORMAT_INFO_MASK_QR = 0x5412;
|
||||
|
||||
/**
|
||||
* Lookup table for decoding format information.
|
||||
*
|
||||
* See ISO 18004:2006, Annex C, Table C.1
|
||||
*/
|
||||
private const FORMAT_INFO_DECODE_LOOKUP = [
|
||||
[0x5412, 0x00],
|
||||
[0x5125, 0x01],
|
||||
[0x5e7c, 0x02],
|
||||
[0x5b4b, 0x03],
|
||||
[0x45f9, 0x04],
|
||||
[0x40ce, 0x05],
|
||||
[0x4f97, 0x06],
|
||||
[0x4aa0, 0x07],
|
||||
[0x77c4, 0x08],
|
||||
[0x72f3, 0x09],
|
||||
[0x7daa, 0x0a],
|
||||
[0x789d, 0x0b],
|
||||
[0x662f, 0x0c],
|
||||
[0x6318, 0x0d],
|
||||
[0x6c41, 0x0e],
|
||||
[0x6976, 0x0f],
|
||||
[0x1689, 0x10],
|
||||
[0x13be, 0x11],
|
||||
[0x1ce7, 0x12],
|
||||
[0x19d0, 0x13],
|
||||
[0x0762, 0x14],
|
||||
[0x0255, 0x15],
|
||||
[0x0d0c, 0x16],
|
||||
[0x083b, 0x17],
|
||||
[0x355f, 0x18],
|
||||
[0x3068, 0x19],
|
||||
[0x3f31, 0x1a],
|
||||
[0x3a06, 0x1b],
|
||||
[0x24b4, 0x1c],
|
||||
[0x2183, 0x1d],
|
||||
[0x2eda, 0x1e],
|
||||
[0x2bed, 0x1f],
|
||||
];
|
||||
|
||||
/**
|
||||
* Offset i holds the number of 1 bits in the binary representation of i.
|
||||
*
|
||||
* @var int[]
|
||||
*/
|
||||
private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
|
||||
|
||||
/**
|
||||
* Error correction level.
|
||||
*/
|
||||
private ErrorCorrectionLevel $ecLevel;
|
||||
|
||||
private int $dataMask;
|
||||
|
||||
protected function __construct(int $formatInfo)
|
||||
{
|
||||
$this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3);
|
||||
$this->dataMask = $formatInfo & 0x7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks how many bits are different between two integers.
|
||||
*/
|
||||
public static function numBitsDiffering(int $a, int $b) : int
|
||||
{
|
||||
$a ^= $b;
|
||||
|
||||
return (
|
||||
self::BITS_SET_IN_HALF_BYTE[$a & 0xf]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
|
||||
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes format information.
|
||||
*/
|
||||
public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
|
||||
{
|
||||
$formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
|
||||
|
||||
if (null !== $formatInfo) {
|
||||
return $formatInfo;
|
||||
}
|
||||
|
||||
// Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the
|
||||
// pattern first.
|
||||
return self::doDecodeFormatInformation(
|
||||
$maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
|
||||
$maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method for decoding format information.
|
||||
*/
|
||||
private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
|
||||
{
|
||||
$bestDifference = PHP_INT_MAX;
|
||||
$bestFormatInfo = 0;
|
||||
|
||||
foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) {
|
||||
$targetInfo = $decodeInfo[0];
|
||||
|
||||
if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
|
||||
// Found an exact match
|
||||
return new self($decodeInfo[1]);
|
||||
}
|
||||
|
||||
$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
|
||||
|
||||
if ($bitsDifference < $bestDifference) {
|
||||
$bestFormatInfo = $decodeInfo[1];
|
||||
$bestDifference = $bitsDifference;
|
||||
}
|
||||
|
||||
if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
|
||||
// Also try the other option
|
||||
$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
|
||||
|
||||
if ($bitsDifference < $bestDifference) {
|
||||
$bestFormatInfo = $decodeInfo[1];
|
||||
$bestDifference = $bitsDifference;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match.
|
||||
if ($bestDifference <= 3) {
|
||||
return new self($bestFormatInfo);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error correction level.
|
||||
*/
|
||||
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
|
||||
{
|
||||
return $this->ecLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data mask.
|
||||
*/
|
||||
public function getDataMask() : int
|
||||
{
|
||||
return $this->dataMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the code of the EC level.
|
||||
*/
|
||||
public function hashCode() : int
|
||||
{
|
||||
return ($this->ecLevel->getBits() << 3) | $this->dataMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if this instance equals another one.
|
||||
*/
|
||||
public function equals(self $other) : bool
|
||||
{
|
||||
return (
|
||||
$this->ecLevel === $other->ecLevel
|
||||
&& $this->dataMask === $other->dataMask
|
||||
);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use DASPRiD\Enum\AbstractEnum;
|
||||
|
||||
/**
|
||||
* Enum representing various modes in which data can be encoded to bits.
|
||||
*
|
||||
* @method static self TERMINATOR()
|
||||
* @method static self NUMERIC()
|
||||
* @method static self ALPHANUMERIC()
|
||||
* @method static self STRUCTURED_APPEND()
|
||||
* @method static self BYTE()
|
||||
* @method static self ECI()
|
||||
* @method static self KANJI()
|
||||
* @method static self FNC1_FIRST_POSITION()
|
||||
* @method static self FNC1_SECOND_POSITION()
|
||||
* @method static self HANZI()
|
||||
*/
|
||||
final class Mode extends AbstractEnum
|
||||
{
|
||||
protected const TERMINATOR = [[0, 0, 0], 0x00];
|
||||
protected const NUMERIC = [[10, 12, 14], 0x01];
|
||||
protected const ALPHANUMERIC = [[9, 11, 13], 0x02];
|
||||
protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03];
|
||||
protected const BYTE = [[8, 16, 16], 0x04];
|
||||
protected const ECI = [[0, 0, 0], 0x07];
|
||||
protected const KANJI = [[8, 10, 12], 0x08];
|
||||
protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05];
|
||||
protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09];
|
||||
protected const HANZI = [[8, 10, 12], 0x0d];
|
||||
|
||||
/**
|
||||
* @param int[] $characterCountBitsForVersions
|
||||
*/
|
||||
protected function __construct(
|
||||
private readonly array $characterCountBitsForVersions,
|
||||
private readonly int $bits
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bits used in a specific QR code version.
|
||||
*/
|
||||
public function getCharacterCountBits(Version $version) : int
|
||||
{
|
||||
$number = $version->getVersionNumber();
|
||||
|
||||
if ($number <= 9) {
|
||||
$offset = 0;
|
||||
} elseif ($number <= 26) {
|
||||
$offset = 1;
|
||||
} else {
|
||||
$offset = 2;
|
||||
}
|
||||
|
||||
return $this->characterCountBitsForVersions[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the four bits used to encode this mode.
|
||||
*/
|
||||
public function getBits() : int
|
||||
{
|
||||
return $this->bits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Reed-Solomon codec for 8-bit characters.
|
||||
*
|
||||
* Based on libfec by Phil Karn, KA9Q.
|
||||
*/
|
||||
final class ReedSolomonCodec
|
||||
{
|
||||
/**
|
||||
* Symbol size in bits.
|
||||
*/
|
||||
private int $symbolSize;
|
||||
|
||||
/**
|
||||
* Block size in symbols.
|
||||
*/
|
||||
private int $blockSize;
|
||||
|
||||
/**
|
||||
* First root of RS code generator polynomial, index form.
|
||||
*/
|
||||
private int $firstRoot;
|
||||
|
||||
/**
|
||||
* Primitive element to generate polynomial roots, index form.
|
||||
*/
|
||||
private int $primitive;
|
||||
|
||||
/**
|
||||
* Prim-th root of 1, index form.
|
||||
*/
|
||||
private int $iPrimitive;
|
||||
|
||||
/**
|
||||
* RS code generator polynomial degree (number of roots).
|
||||
*/
|
||||
private int $numRoots;
|
||||
|
||||
/**
|
||||
* Padding bytes at front of shortened block.
|
||||
*/
|
||||
private int $padding;
|
||||
|
||||
/**
|
||||
* Log lookup table.
|
||||
*
|
||||
* @var SplFixedArray
|
||||
*/
|
||||
private SplFixedArray $alphaTo;
|
||||
|
||||
/**
|
||||
* Anti-Log lookup table.
|
||||
*
|
||||
* @var SplFixedArray
|
||||
*/
|
||||
private SplFixedArray $indexOf;
|
||||
|
||||
/**
|
||||
* Generator polynomial.
|
||||
*
|
||||
* @var SplFixedArray
|
||||
*/
|
||||
private SplFixedArray $generatorPoly;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if symbol size ist not between 0 and 8
|
||||
* @throws InvalidArgumentException if first root is invalid
|
||||
* @throws InvalidArgumentException if num roots is invalid
|
||||
* @throws InvalidArgumentException if padding is invalid
|
||||
* @throws RuntimeException if field generator polynomial is not primitive
|
||||
*/
|
||||
public function __construct(
|
||||
int $symbolSize,
|
||||
int $gfPoly,
|
||||
int $firstRoot,
|
||||
int $primitive,
|
||||
int $numRoots,
|
||||
int $padding
|
||||
) {
|
||||
if ($symbolSize < 0 || $symbolSize > 8) {
|
||||
throw new InvalidArgumentException('Symbol size must be between 0 and 8');
|
||||
}
|
||||
|
||||
if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
|
||||
throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
|
||||
}
|
||||
|
||||
if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
|
||||
throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
|
||||
}
|
||||
|
||||
if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)
|
||||
);
|
||||
}
|
||||
|
||||
$this->symbolSize = $symbolSize;
|
||||
$this->blockSize = (1 << $symbolSize) - 1;
|
||||
$this->padding = $padding;
|
||||
$this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
|
||||
$this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
|
||||
|
||||
// Generate galous field lookup table
|
||||
$this->indexOf[0] = $this->blockSize;
|
||||
$this->alphaTo[$this->blockSize] = 0;
|
||||
|
||||
$sr = 1;
|
||||
|
||||
for ($i = 0; $i < $this->blockSize; ++$i) {
|
||||
$this->indexOf[$sr] = $i;
|
||||
$this->alphaTo[$i] = $sr;
|
||||
|
||||
$sr <<= 1;
|
||||
|
||||
if ($sr & (1 << $symbolSize)) {
|
||||
$sr ^= $gfPoly;
|
||||
}
|
||||
|
||||
$sr &= $this->blockSize;
|
||||
}
|
||||
|
||||
if (1 !== $sr) {
|
||||
throw new RuntimeException('Field generator polynomial is not primitive');
|
||||
}
|
||||
|
||||
// Form RS code generator polynomial from its roots
|
||||
$this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
|
||||
$this->firstRoot = $firstRoot;
|
||||
$this->primitive = $primitive;
|
||||
$this->numRoots = $numRoots;
|
||||
|
||||
// Find prim-th root of 1, used in decoding
|
||||
for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) {
|
||||
}
|
||||
|
||||
$this->iPrimitive = intdiv($iPrimitive, $primitive);
|
||||
|
||||
$this->generatorPoly[0] = 1;
|
||||
|
||||
for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) {
|
||||
$this->generatorPoly[$i + 1] = 1;
|
||||
|
||||
for ($j = $i; $j > 0; $j--) {
|
||||
if ($this->generatorPoly[$j] !== 0) {
|
||||
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[
|
||||
$this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)
|
||||
];
|
||||
} else {
|
||||
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
|
||||
}
|
||||
}
|
||||
|
||||
$this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
|
||||
}
|
||||
|
||||
// Convert generator poly to index form for quicker encoding
|
||||
for ($i = 0; $i <= $numRoots; ++$i) {
|
||||
$this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes data and writes result back into parity array.
|
||||
*/
|
||||
public function encode(SplFixedArray $data, SplFixedArray $parity) : void
|
||||
{
|
||||
for ($i = 0; $i < $this->numRoots; ++$i) {
|
||||
$parity[$i] = 0;
|
||||
}
|
||||
|
||||
$iterations = $this->blockSize - $this->numRoots - $this->padding;
|
||||
|
||||
for ($i = 0; $i < $iterations; ++$i) {
|
||||
$feedback = $this->indexOf[$data[$i] ^ $parity[0]];
|
||||
|
||||
if ($feedback !== $this->blockSize) {
|
||||
// Feedback term is non-zero
|
||||
$feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
|
||||
|
||||
for ($j = 1; $j < $this->numRoots; ++$j) {
|
||||
$parity[$j] = $parity[$j] ^ $this->alphaTo[
|
||||
$this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
for ($j = 0; $j < $this->numRoots - 1; ++$j) {
|
||||
$parity[$j] = $parity[$j + 1];
|
||||
}
|
||||
|
||||
if ($feedback !== $this->blockSize) {
|
||||
$parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
|
||||
} else {
|
||||
$parity[$this->numRoots - 1] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes received data.
|
||||
*/
|
||||
public function decode(SplFixedArray $data, ?SplFixedArray $erasures = null) : ?int
|
||||
{
|
||||
// This speeds up the initialization a bit.
|
||||
$numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
|
||||
$numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
|
||||
|
||||
$lambda = clone $numRootsPlusOne;
|
||||
$b = clone $numRootsPlusOne;
|
||||
$t = clone $numRootsPlusOne;
|
||||
$omega = clone $numRootsPlusOne;
|
||||
$root = clone $numRoots;
|
||||
$loc = clone $numRoots;
|
||||
|
||||
$numErasures = (null !== $erasures ? count($erasures) : 0);
|
||||
|
||||
// Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
|
||||
$syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
|
||||
|
||||
for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) {
|
||||
for ($j = 0; $j < $this->numRoots; ++$j) {
|
||||
if ($syndromes[$j] === 0) {
|
||||
$syndromes[$j] = $data[$i];
|
||||
} else {
|
||||
$syndromes[$j] = $data[$i] ^ $this->alphaTo[
|
||||
$this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert syndromes to index form, checking for nonzero conditions
|
||||
$syndromeError = 0;
|
||||
|
||||
for ($i = 0; $i < $this->numRoots; ++$i) {
|
||||
$syndromeError |= $syndromes[$i];
|
||||
$syndromes[$i] = $this->indexOf[$syndromes[$i]];
|
||||
}
|
||||
|
||||
if (! $syndromeError) {
|
||||
// If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[]
|
||||
// unmodified.
|
||||
return 0;
|
||||
}
|
||||
|
||||
$lambda[0] = 1;
|
||||
|
||||
if ($numErasures > 0) {
|
||||
// Init lambda to be the erasure locator polynomial
|
||||
$lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
|
||||
|
||||
for ($i = 1; $i < $numErasures; ++$i) {
|
||||
$u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
|
||||
|
||||
for ($j = $i + 1; $j > 0; --$j) {
|
||||
$tmp = $this->indexOf[$lambda[$j - 1]];
|
||||
|
||||
if ($tmp !== $this->blockSize) {
|
||||
$lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i <= $this->numRoots; ++$i) {
|
||||
$b[$i] = $this->indexOf[$lambda[$i]];
|
||||
}
|
||||
|
||||
// Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial
|
||||
$r = $numErasures;
|
||||
$el = $numErasures;
|
||||
|
||||
while (++$r <= $this->numRoots) {
|
||||
// Compute discrepancy at the r-th step in poly form
|
||||
$discrepancyR = 0;
|
||||
|
||||
for ($i = 0; $i < $r; ++$i) {
|
||||
if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
|
||||
$discrepancyR ^= $this->alphaTo[
|
||||
$this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$discrepancyR = $this->indexOf[$discrepancyR];
|
||||
|
||||
if ($discrepancyR === $this->blockSize) {
|
||||
$tmp = $b->toArray();
|
||||
array_unshift($tmp, $this->blockSize);
|
||||
array_pop($tmp);
|
||||
$b = SplFixedArray::fromArray($tmp, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
$t[0] = $lambda[0];
|
||||
|
||||
for ($i = 0; $i < $this->numRoots; ++$i) {
|
||||
if ($b[$i] !== $this->blockSize) {
|
||||
$t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
|
||||
} else {
|
||||
$t[$i + 1] = $lambda[$i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (2 * $el <= $r + $numErasures - 1) {
|
||||
$el = $r + $numErasures - $el;
|
||||
|
||||
for ($i = 0; $i <= $this->numRoots; ++$i) {
|
||||
$b[$i] = (
|
||||
$lambda[$i] === 0
|
||||
? $this->blockSize
|
||||
: $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$tmp = $b->toArray();
|
||||
array_unshift($tmp, $this->blockSize);
|
||||
array_pop($tmp);
|
||||
$b = SplFixedArray::fromArray($tmp, false);
|
||||
}
|
||||
|
||||
$lambda = clone $t;
|
||||
}
|
||||
|
||||
// Convert lambda to index form and compute deg(lambda(x))
|
||||
$degLambda = 0;
|
||||
|
||||
for ($i = 0; $i <= $this->numRoots; ++$i) {
|
||||
$lambda[$i] = $this->indexOf[$lambda[$i]];
|
||||
|
||||
if ($lambda[$i] !== $this->blockSize) {
|
||||
$degLambda = $i;
|
||||
}
|
||||
}
|
||||
|
||||
// Find roots of the error+erasure locator polynomial by Chien search.
|
||||
$reg = clone $lambda;
|
||||
$reg[0] = 0;
|
||||
$count = 0;
|
||||
$i = 1;
|
||||
|
||||
for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) {
|
||||
$q = 1;
|
||||
|
||||
for ($j = $degLambda; $j > 0; $j--) {
|
||||
if ($reg[$j] !== $this->blockSize) {
|
||||
$reg[$j] = $this->modNn($reg[$j] + $j);
|
||||
$q ^= $this->alphaTo[$reg[$j]];
|
||||
}
|
||||
}
|
||||
|
||||
if ($q !== 0) {
|
||||
// Not a root
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store root (index-form) and error location number
|
||||
$root[$count] = $i;
|
||||
$loc[$count] = $k;
|
||||
|
||||
if (++$count === $degLambda) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($degLambda !== $count) {
|
||||
// deg(lambda) unequal to number of roots: uncorrectable error detected
|
||||
return null;
|
||||
}
|
||||
|
||||
// Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find
|
||||
// deg(omega).
|
||||
$degOmega = $degLambda - 1;
|
||||
|
||||
for ($i = 0; $i <= $degOmega; ++$i) {
|
||||
$tmp = 0;
|
||||
|
||||
for ($j = $i; $j >= 0; --$j) {
|
||||
if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
|
||||
$tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
|
||||
}
|
||||
}
|
||||
|
||||
$omega[$i] = $this->indexOf[$tmp];
|
||||
}
|
||||
|
||||
// Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and
|
||||
// den = lambda_pr(inv(X(l))) all in poly form.
|
||||
for ($j = $count - 1; $j >= 0; --$j) {
|
||||
$num1 = 0;
|
||||
|
||||
for ($i = $degOmega; $i >= 0; $i--) {
|
||||
if ($omega[$i] !== $this->blockSize) {
|
||||
$num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
|
||||
}
|
||||
}
|
||||
|
||||
$num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
|
||||
$den = 0;
|
||||
|
||||
// lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i]
|
||||
for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
|
||||
if ($lambda[$i + 1] !== $this->blockSize) {
|
||||
$den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply error to data
|
||||
if ($num1 !== 0 && $loc[$j] >= $this->padding) {
|
||||
$data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
|
||||
$this->alphaTo[
|
||||
$this->modNn(
|
||||
$this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $erasures) {
|
||||
if (count($erasures) < $count) {
|
||||
$erasures->setSize($count);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$erasures[$i] = $loc[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide.
|
||||
*/
|
||||
private function modNn(int $x) : int
|
||||
{
|
||||
while ($x >= $this->blockSize) {
|
||||
$x -= $this->blockSize;
|
||||
$x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
|
||||
}
|
||||
|
||||
return $x;
|
||||
}
|
||||
}
|
||||
+592
@@ -0,0 +1,592 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Common;
|
||||
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Version representation.
|
||||
*/
|
||||
final class Version
|
||||
{
|
||||
private const VERSION_DECODE_INFO = [
|
||||
0x07c94,
|
||||
0x085bc,
|
||||
0x09a99,
|
||||
0x0a4d3,
|
||||
0x0bbf6,
|
||||
0x0c762,
|
||||
0x0d847,
|
||||
0x0e60d,
|
||||
0x0f928,
|
||||
0x10b78,
|
||||
0x1145d,
|
||||
0x12a17,
|
||||
0x13532,
|
||||
0x149a6,
|
||||
0x15683,
|
||||
0x168c9,
|
||||
0x177ec,
|
||||
0x18ec4,
|
||||
0x191e1,
|
||||
0x1afab,
|
||||
0x1b08e,
|
||||
0x1cc1a,
|
||||
0x1d33f,
|
||||
0x1ed75,
|
||||
0x1f250,
|
||||
0x209d5,
|
||||
0x216f0,
|
||||
0x228ba,
|
||||
0x2379f,
|
||||
0x24b0b,
|
||||
0x2542e,
|
||||
0x26a64,
|
||||
0x27541,
|
||||
0x28c69,
|
||||
];
|
||||
|
||||
/**
|
||||
* Version number of this version.
|
||||
*/
|
||||
private int $versionNumber;
|
||||
|
||||
/**
|
||||
* Alignment pattern centers.
|
||||
*
|
||||
* @var SplFixedArray|array
|
||||
*/
|
||||
private SplFixedArray|array $alignmentPatternCenters;
|
||||
|
||||
/**
|
||||
* Error correction blocks.
|
||||
*
|
||||
* @var EcBlocks[]
|
||||
*/
|
||||
private array $ecBlocks;
|
||||
|
||||
/**
|
||||
* Total number of codewords.
|
||||
*/
|
||||
private null|int|float $totalCodewords;
|
||||
|
||||
/**
|
||||
* Cached version instances.
|
||||
*
|
||||
* @var array<int, self>|null
|
||||
*/
|
||||
private static ?array $versions = null;
|
||||
|
||||
/**
|
||||
* @param int[] $alignmentPatternCenters
|
||||
*/
|
||||
private function __construct(
|
||||
int $versionNumber,
|
||||
array $alignmentPatternCenters,
|
||||
EcBlocks ...$ecBlocks
|
||||
) {
|
||||
$this->versionNumber = $versionNumber;
|
||||
$this->alignmentPatternCenters = $alignmentPatternCenters;
|
||||
$this->ecBlocks = $ecBlocks;
|
||||
|
||||
$totalCodewords = 0;
|
||||
$ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock();
|
||||
|
||||
foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) {
|
||||
$totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords);
|
||||
}
|
||||
|
||||
$this->totalCodewords = $totalCodewords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version number.
|
||||
*/
|
||||
public function getVersionNumber() : int
|
||||
{
|
||||
return $this->versionNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alignment pattern centers.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function getAlignmentPatternCenters() : array
|
||||
{
|
||||
return $this->alignmentPatternCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of codewords.
|
||||
*/
|
||||
public function getTotalCodewords() : int
|
||||
{
|
||||
return $this->totalCodewords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dimension for the current version.
|
||||
*/
|
||||
public function getDimensionForVersion() : int
|
||||
{
|
||||
return 17 + 4 * $this->versionNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of EC blocks for a specific EC level.
|
||||
*/
|
||||
public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks
|
||||
{
|
||||
return $this->ecBlocks[$ecLevel->ordinal()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a provisional version number for a specific dimension.
|
||||
*
|
||||
* @throws InvalidArgumentException if dimension is not 1 mod 4
|
||||
*/
|
||||
public static function getProvisionalVersionForDimension(int $dimension) : self
|
||||
{
|
||||
if (1 !== $dimension % 4) {
|
||||
throw new InvalidArgumentException('Dimension is not 1 mod 4');
|
||||
}
|
||||
|
||||
return self::getVersionForNumber(intdiv($dimension - 17, 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a version instance for a specific version number.
|
||||
*
|
||||
* @throws InvalidArgumentException if version number is out of range
|
||||
*/
|
||||
public static function getVersionForNumber(int $versionNumber) : self
|
||||
{
|
||||
if ($versionNumber < 1 || $versionNumber > 40) {
|
||||
throw new InvalidArgumentException('Version number must be between 1 and 40');
|
||||
}
|
||||
|
||||
return self::versions()[$versionNumber - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes version information from an integer and returns the version.
|
||||
*/
|
||||
public static function decodeVersionInformation(int $versionBits) : ?self
|
||||
{
|
||||
$bestDifference = PHP_INT_MAX;
|
||||
$bestVersion = 0;
|
||||
|
||||
foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) {
|
||||
if ($targetVersion === $versionBits) {
|
||||
return self::getVersionForNumber($i + 7);
|
||||
}
|
||||
|
||||
$bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion);
|
||||
|
||||
if ($bitsDifference < $bestDifference) {
|
||||
$bestVersion = $i + 7;
|
||||
$bestDifference = $bitsDifference;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestDifference <= 3) {
|
||||
return self::getVersionForNumber($bestVersion);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the function pattern for the current version.
|
||||
*/
|
||||
public function buildFunctionPattern() : BitMatrix
|
||||
{
|
||||
$dimension = $this->getDimensionForVersion();
|
||||
$bitMatrix = new BitMatrix($dimension);
|
||||
|
||||
// Top left finder pattern + separator + format
|
||||
$bitMatrix->setRegion(0, 0, 9, 9);
|
||||
// Top right finder pattern + separator + format
|
||||
$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
|
||||
// Bottom left finder pattern + separator + format
|
||||
$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
|
||||
|
||||
// Alignment patterns
|
||||
$max = count($this->alignmentPatternCenters);
|
||||
|
||||
for ($x = 0; $x < $max; ++$x) {
|
||||
$i = $this->alignmentPatternCenters[$x] - 2;
|
||||
|
||||
for ($y = 0; $y < $max; ++$y) {
|
||||
if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) {
|
||||
// No alignment patterns near the three finder paterns
|
||||
continue;
|
||||
}
|
||||
|
||||
$bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5);
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical timing pattern
|
||||
$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
|
||||
// Horizontal timing pattern
|
||||
$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
|
||||
|
||||
if ($this->versionNumber > 6) {
|
||||
// Version info, top right
|
||||
$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
|
||||
// Version info, bottom left
|
||||
$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
|
||||
}
|
||||
|
||||
return $bitMatrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation for the version.
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
return (string) $this->versionNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and cache a specific version.
|
||||
*
|
||||
* See ISO 18004:2006 6.5.1 Table 9.
|
||||
*
|
||||
* @return array<int, self>
|
||||
*/
|
||||
private static function versions() : array
|
||||
{
|
||||
if (null !== self::$versions) {
|
||||
return self::$versions;
|
||||
}
|
||||
|
||||
return self::$versions = [
|
||||
new self(
|
||||
1,
|
||||
[],
|
||||
new EcBlocks(7, new EcBlock(1, 19)),
|
||||
new EcBlocks(10, new EcBlock(1, 16)),
|
||||
new EcBlocks(13, new EcBlock(1, 13)),
|
||||
new EcBlocks(17, new EcBlock(1, 9))
|
||||
),
|
||||
new self(
|
||||
2,
|
||||
[6, 18],
|
||||
new EcBlocks(10, new EcBlock(1, 34)),
|
||||
new EcBlocks(16, new EcBlock(1, 28)),
|
||||
new EcBlocks(22, new EcBlock(1, 22)),
|
||||
new EcBlocks(28, new EcBlock(1, 16))
|
||||
),
|
||||
new self(
|
||||
3,
|
||||
[6, 22],
|
||||
new EcBlocks(15, new EcBlock(1, 55)),
|
||||
new EcBlocks(26, new EcBlock(1, 44)),
|
||||
new EcBlocks(18, new EcBlock(2, 17)),
|
||||
new EcBlocks(22, new EcBlock(2, 13))
|
||||
),
|
||||
new self(
|
||||
4,
|
||||
[6, 26],
|
||||
new EcBlocks(20, new EcBlock(1, 80)),
|
||||
new EcBlocks(18, new EcBlock(2, 32)),
|
||||
new EcBlocks(26, new EcBlock(2, 24)),
|
||||
new EcBlocks(16, new EcBlock(4, 9))
|
||||
),
|
||||
new self(
|
||||
5,
|
||||
[6, 30],
|
||||
new EcBlocks(26, new EcBlock(1, 108)),
|
||||
new EcBlocks(24, new EcBlock(2, 43)),
|
||||
new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)),
|
||||
new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12))
|
||||
),
|
||||
new self(
|
||||
6,
|
||||
[6, 34],
|
||||
new EcBlocks(18, new EcBlock(2, 68)),
|
||||
new EcBlocks(16, new EcBlock(4, 27)),
|
||||
new EcBlocks(24, new EcBlock(4, 19)),
|
||||
new EcBlocks(28, new EcBlock(4, 15))
|
||||
),
|
||||
new self(
|
||||
7,
|
||||
[6, 22, 38],
|
||||
new EcBlocks(20, new EcBlock(2, 78)),
|
||||
new EcBlocks(18, new EcBlock(4, 31)),
|
||||
new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)),
|
||||
new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14))
|
||||
),
|
||||
new self(
|
||||
8,
|
||||
[6, 24, 42],
|
||||
new EcBlocks(24, new EcBlock(2, 97)),
|
||||
new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)),
|
||||
new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)),
|
||||
new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15))
|
||||
),
|
||||
new self(
|
||||
9,
|
||||
[6, 26, 46],
|
||||
new EcBlocks(30, new EcBlock(2, 116)),
|
||||
new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)),
|
||||
new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)),
|
||||
new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13))
|
||||
),
|
||||
new self(
|
||||
10,
|
||||
[6, 28, 50],
|
||||
new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)),
|
||||
new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)),
|
||||
new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)),
|
||||
new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16))
|
||||
),
|
||||
new self(
|
||||
11,
|
||||
[6, 30, 54],
|
||||
new EcBlocks(20, new EcBlock(4, 81)),
|
||||
new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)),
|
||||
new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)),
|
||||
new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13))
|
||||
),
|
||||
new self(
|
||||
12,
|
||||
[6, 32, 58],
|
||||
new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)),
|
||||
new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)),
|
||||
new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)),
|
||||
new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15))
|
||||
),
|
||||
new self(
|
||||
13,
|
||||
[6, 34, 62],
|
||||
new EcBlocks(26, new EcBlock(4, 107)),
|
||||
new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)),
|
||||
new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)),
|
||||
new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12))
|
||||
),
|
||||
new self(
|
||||
14,
|
||||
[6, 26, 46, 66],
|
||||
new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)),
|
||||
new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)),
|
||||
new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)),
|
||||
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13))
|
||||
),
|
||||
new self(
|
||||
15,
|
||||
[6, 26, 48, 70],
|
||||
new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)),
|
||||
new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)),
|
||||
new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)),
|
||||
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13))
|
||||
),
|
||||
new self(
|
||||
16,
|
||||
[6, 26, 50, 74],
|
||||
new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)),
|
||||
new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)),
|
||||
new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)),
|
||||
new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16))
|
||||
),
|
||||
new self(
|
||||
17,
|
||||
[6, 30, 54, 78],
|
||||
new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)),
|
||||
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)),
|
||||
new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)),
|
||||
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15))
|
||||
),
|
||||
new self(
|
||||
18,
|
||||
[6, 30, 56, 82],
|
||||
new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)),
|
||||
new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)),
|
||||
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)),
|
||||
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15))
|
||||
),
|
||||
new self(
|
||||
19,
|
||||
[6, 30, 58, 86],
|
||||
new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)),
|
||||
new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)),
|
||||
new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)),
|
||||
new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14))
|
||||
),
|
||||
new self(
|
||||
20,
|
||||
[6, 34, 62, 90],
|
||||
new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)),
|
||||
new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)),
|
||||
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)),
|
||||
new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16))
|
||||
),
|
||||
new self(
|
||||
21,
|
||||
[6, 28, 50, 72, 94],
|
||||
new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)),
|
||||
new EcBlocks(26, new EcBlock(17, 42)),
|
||||
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)),
|
||||
new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17))
|
||||
),
|
||||
new self(
|
||||
22,
|
||||
[6, 26, 50, 74, 98],
|
||||
new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)),
|
||||
new EcBlocks(28, new EcBlock(17, 46)),
|
||||
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)),
|
||||
new EcBlocks(24, new EcBlock(34, 13))
|
||||
),
|
||||
new self(
|
||||
23,
|
||||
[6, 30, 54, 78, 102],
|
||||
new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)),
|
||||
new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)),
|
||||
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)),
|
||||
new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16))
|
||||
),
|
||||
new self(
|
||||
24,
|
||||
[6, 28, 54, 80, 106],
|
||||
new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)),
|
||||
new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)),
|
||||
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)),
|
||||
new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17))
|
||||
),
|
||||
new self(
|
||||
25,
|
||||
[6, 32, 58, 84, 110],
|
||||
new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)),
|
||||
new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)),
|
||||
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)),
|
||||
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16))
|
||||
),
|
||||
new self(
|
||||
26,
|
||||
[6, 30, 58, 86, 114],
|
||||
new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)),
|
||||
new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)),
|
||||
new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)),
|
||||
new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17))
|
||||
),
|
||||
new self(
|
||||
27,
|
||||
[6, 34, 62, 90, 118],
|
||||
new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)),
|
||||
new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)),
|
||||
new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)),
|
||||
new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16))
|
||||
),
|
||||
new self(
|
||||
28,
|
||||
[6, 26, 50, 74, 98, 122],
|
||||
new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)),
|
||||
new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)),
|
||||
new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)),
|
||||
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16))
|
||||
),
|
||||
new self(
|
||||
29,
|
||||
[6, 30, 54, 78, 102, 126],
|
||||
new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)),
|
||||
new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)),
|
||||
new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)),
|
||||
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16))
|
||||
),
|
||||
new self(
|
||||
30,
|
||||
[6, 26, 52, 78, 104, 130],
|
||||
new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)),
|
||||
new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)),
|
||||
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)),
|
||||
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16))
|
||||
),
|
||||
new self(
|
||||
31,
|
||||
[6, 30, 56, 82, 108, 134],
|
||||
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)),
|
||||
new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)),
|
||||
new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)),
|
||||
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16))
|
||||
),
|
||||
new self(
|
||||
32,
|
||||
[6, 34, 60, 86, 112, 138],
|
||||
new EcBlocks(30, new EcBlock(17, 115)),
|
||||
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)),
|
||||
new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)),
|
||||
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16))
|
||||
),
|
||||
new self(
|
||||
33,
|
||||
[6, 30, 58, 86, 114, 142],
|
||||
new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)),
|
||||
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)),
|
||||
new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)),
|
||||
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16))
|
||||
),
|
||||
new self(
|
||||
34,
|
||||
[6, 34, 62, 90, 118, 146],
|
||||
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)),
|
||||
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)),
|
||||
new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)),
|
||||
new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17))
|
||||
),
|
||||
new self(
|
||||
35,
|
||||
[6, 30, 54, 78, 102, 126, 150],
|
||||
new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)),
|
||||
new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)),
|
||||
new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)),
|
||||
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16))
|
||||
),
|
||||
new self(
|
||||
36,
|
||||
[6, 24, 50, 76, 102, 128, 154],
|
||||
new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)),
|
||||
new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)),
|
||||
new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)),
|
||||
new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16))
|
||||
),
|
||||
new self(
|
||||
37,
|
||||
[6, 28, 54, 80, 106, 132, 158],
|
||||
new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)),
|
||||
new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)),
|
||||
new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)),
|
||||
new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16))
|
||||
),
|
||||
new self(
|
||||
38,
|
||||
[6, 32, 58, 84, 110, 136, 162],
|
||||
new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)),
|
||||
new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)),
|
||||
new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)),
|
||||
new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16))
|
||||
),
|
||||
new self(
|
||||
39,
|
||||
[6, 26, 54, 82, 110, 138, 166],
|
||||
new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)),
|
||||
new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)),
|
||||
new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)),
|
||||
new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16))
|
||||
),
|
||||
new self(
|
||||
40,
|
||||
[6, 30, 58, 86, 114, 142, 170],
|
||||
new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)),
|
||||
new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)),
|
||||
new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)),
|
||||
new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16))
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Encoder;
|
||||
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Block pair.
|
||||
*/
|
||||
final class BlockPair
|
||||
{
|
||||
/**
|
||||
* Creates a new block pair.
|
||||
*
|
||||
* @param SplFixedArray<int> $dataBytes Data bytes in the block.
|
||||
* @param SplFixedArray<int> $errorCorrectionBytes Error correction bytes in the block.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly SplFixedArray $dataBytes,
|
||||
private readonly SplFixedArray $errorCorrectionBytes
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data bytes.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function getDataBytes() : SplFixedArray
|
||||
{
|
||||
return $this->dataBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error correction bytes.
|
||||
*
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
public function getErrorCorrectionBytes() : SplFixedArray
|
||||
{
|
||||
return $this->errorCorrectionBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Encoder;
|
||||
|
||||
use SplFixedArray;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Byte matrix.
|
||||
*/
|
||||
final class ByteMatrix
|
||||
{
|
||||
/**
|
||||
* Bytes in the matrix, represented as array.
|
||||
*
|
||||
* @var SplFixedArray<SplFixedArray<int>>
|
||||
*/
|
||||
private SplFixedArray $bytes;
|
||||
|
||||
public function __construct(private readonly int $width, private readonly int $height)
|
||||
{
|
||||
$this->bytes = new SplFixedArray($height);
|
||||
|
||||
for ($y = 0; $y < $height; ++$y) {
|
||||
$this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the matrix.
|
||||
*/
|
||||
public function getWidth() : int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of the matrix.
|
||||
*/
|
||||
public function getHeight() : int
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal representation of the matrix.
|
||||
*
|
||||
* @return SplFixedArray<SplFixedArray<int>>
|
||||
*/
|
||||
public function getArray() : SplFixedArray
|
||||
{
|
||||
return $this->bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable<int>
|
||||
*/
|
||||
public function getBytes() : Traversable
|
||||
{
|
||||
foreach ($this->bytes as $row) {
|
||||
foreach ($row as $byte) {
|
||||
yield $byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the byte for a specific position.
|
||||
*/
|
||||
public function get(int $x, int $y) : int
|
||||
{
|
||||
return $this->bytes[$y][$x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the byte for a specific position.
|
||||
*/
|
||||
public function set(int $x, int $y, int $value) : void
|
||||
{
|
||||
$this->bytes[$y][$x] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the matrix with a specific value.
|
||||
*/
|
||||
public function clear(int $value) : void
|
||||
{
|
||||
for ($y = 0; $y < $this->height; ++$y) {
|
||||
for ($x = 0; $x < $this->width; ++$x) {
|
||||
$this->bytes[$y][$x] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->bytes = clone $this->bytes;
|
||||
|
||||
foreach ($this->bytes as $index => $row) {
|
||||
$this->bytes[$index] = clone $row;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the matrix.
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
for ($y = 0; $y < $this->height; $y++) {
|
||||
for ($x = 0; $x < $this->width; $x++) {
|
||||
switch ($this->bytes[$y][$x]) {
|
||||
case 0:
|
||||
$result .= ' 0';
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$result .= ' 1';
|
||||
break;
|
||||
|
||||
default:
|
||||
$result .= ' ';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$result .= "\n";
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+666
@@ -0,0 +1,666 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Encoder;
|
||||
|
||||
use BaconQrCode\Common\BitArray;
|
||||
use BaconQrCode\Common\CharacterSetEci;
|
||||
use BaconQrCode\Common\ErrorCorrectionLevel;
|
||||
use BaconQrCode\Common\Mode;
|
||||
use BaconQrCode\Common\ReedSolomonCodec;
|
||||
use BaconQrCode\Common\Version;
|
||||
use BaconQrCode\Exception\WriterException;
|
||||
use SplFixedArray;
|
||||
|
||||
/**
|
||||
* Encoder.
|
||||
*/
|
||||
final class Encoder
|
||||
{
|
||||
/**
|
||||
* Default byte encoding.
|
||||
*/
|
||||
public const DEFAULT_BYTE_MODE_ENCODING = 'ISO-8859-1';
|
||||
|
||||
/** @deprecated use DEFAULT_BYTE_MODE_ENCODING */
|
||||
public const DEFAULT_BYTE_MODE_ECODING = self::DEFAULT_BYTE_MODE_ENCODING;
|
||||
|
||||
/**
|
||||
* Allowed characters for the Alphanumeric Mode.
|
||||
*/
|
||||
private const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
||||
|
||||
/**
|
||||
* The original table is defined in the table 5 of JISX0510:2004 (p.19).
|
||||
*/
|
||||
private const ALPHANUMERIC_TABLE = [
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f
|
||||
36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f
|
||||
-1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f
|
||||
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f
|
||||
];
|
||||
|
||||
/**
|
||||
* Codec cache.
|
||||
*
|
||||
* @var array<string,ReedSolomonCodec>
|
||||
*/
|
||||
private static array $codecs = [];
|
||||
|
||||
/**
|
||||
* Encodes "content" with the error correction level "ecLevel".
|
||||
*/
|
||||
public static function encode(
|
||||
string $content,
|
||||
ErrorCorrectionLevel $ecLevel,
|
||||
string $encoding = self::DEFAULT_BYTE_MODE_ENCODING,
|
||||
?Version $forcedVersion = null,
|
||||
// Barcode scanner might not be able to read the encoded message of the QR code with the prefix ECI of UTF-8
|
||||
bool $prefixEci = true
|
||||
) : QrCode {
|
||||
// Pick an encoding mode appropriate for the content. Note that this
|
||||
// will not attempt to use multiple modes / segments even if that were
|
||||
// more efficient. Would be nice.
|
||||
$mode = self::chooseMode($content, $encoding);
|
||||
|
||||
// This will store the header information, like mode and length, as well
|
||||
// as "header" segments like an ECI segment.
|
||||
$headerBits = new BitArray();
|
||||
|
||||
// Append ECI segment if applicable
|
||||
if ($prefixEci && Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ENCODING !== $encoding) {
|
||||
$eci = CharacterSetEci::getCharacterSetEciByName($encoding);
|
||||
|
||||
if (null !== $eci) {
|
||||
self::appendEci($eci, $headerBits);
|
||||
}
|
||||
}
|
||||
|
||||
// (With ECI in place,) Write the mode marker
|
||||
self::appendModeInfo($mode, $headerBits);
|
||||
|
||||
// Collect data within the main segment, separately, to count its size
|
||||
// if needed. Don't add it to main payload yet.
|
||||
$dataBits = new BitArray();
|
||||
self::appendBytes($content, $mode, $dataBits, $encoding);
|
||||
|
||||
// Hard part: need to know version to know how many bits length takes.
|
||||
// But need to know how many bits it takes to know version. First we
|
||||
// take a guess at version by assuming version will be the minimum, 1:
|
||||
$provisionalBitsNeeded = $headerBits->getSize()
|
||||
+ $mode->getCharacterCountBits(Version::getVersionForNumber(1))
|
||||
+ $dataBits->getSize();
|
||||
$provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
|
||||
|
||||
// Use that guess to calculate the right version. I am still not sure
|
||||
// this works in 100% of cases.
|
||||
$bitsNeeded = $headerBits->getSize()
|
||||
+ $mode->getCharacterCountBits($provisionalVersion)
|
||||
+ $dataBits->getSize();
|
||||
$version = self::chooseVersion($bitsNeeded, $ecLevel);
|
||||
|
||||
if (null !== $forcedVersion) {
|
||||
// Forced version check
|
||||
if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) {
|
||||
// Calculated minimum version is same or equal as forced version
|
||||
$version = $forcedVersion;
|
||||
} else {
|
||||
throw new WriterException(
|
||||
'Invalid version! Calculated version: '
|
||||
. $version->getVersionNumber()
|
||||
. ', requested version: '
|
||||
. $forcedVersion->getVersionNumber()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$headerAndDataBits = new BitArray();
|
||||
$headerAndDataBits->appendBitArray($headerBits);
|
||||
|
||||
// Find "length" of main segment and write it.
|
||||
$numLetters = match ($mode) {
|
||||
Mode::BYTE() => $dataBits->getSizeInBytes(),
|
||||
Mode::NUMERIC(), Mode::ALPHANUMERIC() => strlen($content),
|
||||
Mode::KANJI() => iconv_strlen($content, 'utf-8'),
|
||||
};
|
||||
self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
|
||||
|
||||
// Put data together into the overall payload.
|
||||
$headerAndDataBits->appendBitArray($dataBits);
|
||||
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
|
||||
$numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
|
||||
|
||||
// Terminate the bits properly.
|
||||
self::terminateBits($numDataBytes, $headerAndDataBits);
|
||||
|
||||
// Interleave data bits with error correction code.
|
||||
$finalBits = self::interleaveWithEcBytes(
|
||||
$headerAndDataBits,
|
||||
$version->getTotalCodewords(),
|
||||
$numDataBytes,
|
||||
$ecBlocks->getNumBlocks()
|
||||
);
|
||||
|
||||
// Choose the mask pattern.
|
||||
$dimension = $version->getDimensionForVersion();
|
||||
$matrix = new ByteMatrix($dimension, $dimension);
|
||||
$maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
|
||||
|
||||
// Build the matrix.
|
||||
MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
|
||||
|
||||
return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the alphanumeric code for a byte.
|
||||
*/
|
||||
private static function getAlphanumericCode(int $byte) : int
|
||||
{
|
||||
return self::ALPHANUMERIC_TABLE[$byte] ?? -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the best mode for a given content.
|
||||
*/
|
||||
private static function chooseMode(string $content, ?string $encoding = null) : Mode
|
||||
{
|
||||
if ('' === $content) {
|
||||
return Mode::BYTE();
|
||||
}
|
||||
|
||||
if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) {
|
||||
return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
|
||||
}
|
||||
|
||||
if (ctype_digit($content)) {
|
||||
return Mode::NUMERIC();
|
||||
}
|
||||
|
||||
if (self::isOnlyAlphanumeric($content)) {
|
||||
return Mode::ALPHANUMERIC();
|
||||
}
|
||||
|
||||
return Mode::BYTE();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the mask penalty for a matrix.
|
||||
*/
|
||||
private static function calculateMaskPenalty(ByteMatrix $matrix) : int
|
||||
{
|
||||
return (
|
||||
MaskUtil::applyMaskPenaltyRule1($matrix)
|
||||
+ MaskUtil::applyMaskPenaltyRule2($matrix)
|
||||
+ MaskUtil::applyMaskPenaltyRule3($matrix)
|
||||
+ MaskUtil::applyMaskPenaltyRule4($matrix)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if content only consists of double-byte kanji characters (or is empty).
|
||||
*/
|
||||
private static function isOnlyDoubleByteKanji(string $content) : bool
|
||||
{
|
||||
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
|
||||
|
||||
if (false === $bytes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$length = strlen($bytes);
|
||||
|
||||
if (0 !== $length % 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $length; $i += 2) {
|
||||
$byte = ord($bytes[$i]);
|
||||
|
||||
if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if content only consists of alphanumeric characters (or is empty).
|
||||
*/
|
||||
private static function isOnlyAlphanumeric(string $content) : bool
|
||||
{
|
||||
return strlen($content) === strspn($content, self::ALPHANUMERIC_CHARS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the best mask pattern for a matrix.
|
||||
*/
|
||||
private static function chooseMaskPattern(
|
||||
BitArray $bits,
|
||||
ErrorCorrectionLevel $ecLevel,
|
||||
Version $version,
|
||||
ByteMatrix $matrix
|
||||
) : int {
|
||||
$minPenalty = PHP_INT_MAX;
|
||||
$bestMaskPattern = -1;
|
||||
|
||||
for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) {
|
||||
MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
|
||||
$penalty = self::calculateMaskPenalty($matrix);
|
||||
|
||||
if ($penalty < $minPenalty) {
|
||||
$minPenalty = $penalty;
|
||||
$bestMaskPattern = $maskPattern;
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMaskPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the best version for the input.
|
||||
*
|
||||
* @throws WriterException if data is too big
|
||||
*/
|
||||
private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version
|
||||
{
|
||||
for ($versionNum = 1; $versionNum <= 40; ++$versionNum) {
|
||||
$version = Version::getVersionForNumber($versionNum);
|
||||
$numBytes = $version->getTotalCodewords();
|
||||
|
||||
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
|
||||
$numEcBytes = $ecBlocks->getTotalEcCodewords();
|
||||
|
||||
$numDataBytes = $numBytes - $numEcBytes;
|
||||
$totalInputBytes = intdiv($numInputBits + 8, 8);
|
||||
|
||||
if ($numDataBytes >= $totalInputBytes) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
|
||||
throw new WriterException('Data too big');
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the bits in a bit array.
|
||||
*
|
||||
* @throws WriterException if data bits cannot fit in the QR code
|
||||
* @throws WriterException if bits size does not equal the capacity
|
||||
*/
|
||||
private static function terminateBits(int $numDataBytes, BitArray $bits) : void
|
||||
{
|
||||
$capacity = $numDataBytes << 3;
|
||||
|
||||
if ($bits->getSize() > $capacity) {
|
||||
throw new WriterException('Data bits cannot fit in the QR code');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) {
|
||||
$bits->appendBit(false);
|
||||
}
|
||||
|
||||
$numBitsInLastByte = $bits->getSize() & 0x7;
|
||||
|
||||
if ($numBitsInLastByte > 0) {
|
||||
for ($i = $numBitsInLastByte; $i < 8; ++$i) {
|
||||
$bits->appendBit(false);
|
||||
}
|
||||
}
|
||||
|
||||
$numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
|
||||
|
||||
for ($i = 0; $i < $numPaddingBytes; ++$i) {
|
||||
$bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8);
|
||||
}
|
||||
|
||||
if ($bits->getSize() !== $capacity) {
|
||||
throw new WriterException('Bits size does not equal capacity');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets number of data- and EC bytes for a block ID.
|
||||
*
|
||||
* @return int[]
|
||||
* @throws WriterException if block ID is too large
|
||||
* @throws WriterException if EC bytes mismatch
|
||||
* @throws WriterException if RS blocks mismatch
|
||||
* @throws WriterException if total bytes mismatch
|
||||
*/
|
||||
private static function getNumDataBytesAndNumEcBytesForBlockId(
|
||||
int $numTotalBytes,
|
||||
int $numDataBytes,
|
||||
int $numRsBlocks,
|
||||
int $blockId
|
||||
) : array {
|
||||
if ($blockId >= $numRsBlocks) {
|
||||
throw new WriterException('Block ID too large');
|
||||
}
|
||||
|
||||
$numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
|
||||
$numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
|
||||
$numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks);
|
||||
$numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
|
||||
$numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks);
|
||||
$numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
|
||||
$numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
|
||||
$numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
|
||||
|
||||
if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
|
||||
throw new WriterException('EC bytes mismatch');
|
||||
}
|
||||
|
||||
if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
|
||||
throw new WriterException('RS blocks mismatch');
|
||||
}
|
||||
|
||||
if ($numTotalBytes !==
|
||||
(($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1)
|
||||
+ (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2)
|
||||
) {
|
||||
throw new WriterException('Total bytes mismatch');
|
||||
}
|
||||
|
||||
if ($blockId < $numRsBlocksInGroup1) {
|
||||
return [$numDataBytesInGroup1, $numEcBytesInGroup1];
|
||||
} else {
|
||||
return [$numDataBytesInGroup2, $numEcBytesInGroup2];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interleaves data with EC bytes.
|
||||
*
|
||||
* @throws WriterException if number of bits and data bytes does not match
|
||||
* @throws WriterException if data bytes does not match offset
|
||||
* @throws WriterException if an interleaving error occurs
|
||||
*/
|
||||
private static function interleaveWithEcBytes(
|
||||
BitArray $bits,
|
||||
int $numTotalBytes,
|
||||
int $numDataBytes,
|
||||
int $numRsBlocks
|
||||
) : BitArray {
|
||||
if ($bits->getSizeInBytes() !== $numDataBytes) {
|
||||
throw new WriterException('Number of bits and data bytes does not match');
|
||||
}
|
||||
|
||||
$dataBytesOffset = 0;
|
||||
$maxNumDataBytes = 0;
|
||||
$maxNumEcBytes = 0;
|
||||
|
||||
$blocks = new SplFixedArray($numRsBlocks);
|
||||
|
||||
for ($i = 0; $i < $numRsBlocks; ++$i) {
|
||||
list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId(
|
||||
$numTotalBytes,
|
||||
$numDataBytes,
|
||||
$numRsBlocks,
|
||||
$i
|
||||
);
|
||||
|
||||
$size = $numDataBytesInBlock;
|
||||
$dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
|
||||
$ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
|
||||
$blocks[$i] = new BlockPair($dataBytes, $ecBytes);
|
||||
|
||||
$maxNumDataBytes = max($maxNumDataBytes, $size);
|
||||
$maxNumEcBytes = max($maxNumEcBytes, count($ecBytes));
|
||||
$dataBytesOffset += $numDataBytesInBlock;
|
||||
}
|
||||
|
||||
if ($numDataBytes !== $dataBytesOffset) {
|
||||
throw new WriterException('Data bytes does not match offset');
|
||||
}
|
||||
|
||||
$result = new BitArray();
|
||||
|
||||
for ($i = 0; $i < $maxNumDataBytes; ++$i) {
|
||||
foreach ($blocks as $block) {
|
||||
$dataBytes = $block->getDataBytes();
|
||||
|
||||
if ($i < count($dataBytes)) {
|
||||
$result->appendBits($dataBytes[$i], 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $maxNumEcBytes; ++$i) {
|
||||
foreach ($blocks as $block) {
|
||||
$ecBytes = $block->getErrorCorrectionBytes();
|
||||
|
||||
if ($i < count($ecBytes)) {
|
||||
$result->appendBits($ecBytes[$i], 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($numTotalBytes !== $result->getSizeInBytes()) {
|
||||
throw new WriterException(
|
||||
'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ'
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates EC bytes for given data.
|
||||
*
|
||||
* @param SplFixedArray<int> $dataBytes
|
||||
* @return SplFixedArray<int>
|
||||
*/
|
||||
private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray
|
||||
{
|
||||
$numDataBytes = count($dataBytes);
|
||||
$toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
|
||||
|
||||
for ($i = 0; $i < $numDataBytes; $i++) {
|
||||
$toEncode[$i] = $dataBytes[$i];
|
||||
}
|
||||
|
||||
$ecBytes = new SplFixedArray($numEcBytesInBlock);
|
||||
$codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
|
||||
$codec->encode($toEncode, $ecBytes);
|
||||
|
||||
return $ecBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an RS codec and caches it.
|
||||
*/
|
||||
private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec
|
||||
{
|
||||
$cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
|
||||
|
||||
if (isset(self::$codecs[$cacheId])) {
|
||||
return self::$codecs[$cacheId];
|
||||
}
|
||||
|
||||
return self::$codecs[$cacheId] = new ReedSolomonCodec(
|
||||
8,
|
||||
0x11d,
|
||||
0,
|
||||
1,
|
||||
$numEcBytesInBlock,
|
||||
255 - $numDataBytes - $numEcBytesInBlock
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends mode information to a bit array.
|
||||
*/
|
||||
private static function appendModeInfo(Mode $mode, BitArray $bits) : void
|
||||
{
|
||||
$bits->appendBits($mode->getBits(), 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends length information to a bit array.
|
||||
*
|
||||
* @throws WriterException if num letters is bigger than expected
|
||||
*/
|
||||
private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void
|
||||
{
|
||||
$numBits = $mode->getCharacterCountBits($version);
|
||||
|
||||
if ($numLetters >= (1 << $numBits)) {
|
||||
throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
|
||||
}
|
||||
|
||||
$bits->appendBits($numLetters, $numBits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends bytes to a bit array in a specific mode.
|
||||
*/
|
||||
private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
|
||||
{
|
||||
match ($mode) {
|
||||
Mode::NUMERIC() => self::appendNumericBytes($content, $bits),
|
||||
Mode::ALPHANUMERIC() => self::appendAlphanumericBytes($content, $bits),
|
||||
Mode::BYTE() => self::append8BitBytes($content, $bits, $encoding),
|
||||
Mode::KANJI() => self::appendKanjiBytes($content, $bits),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends numeric bytes to a bit array.
|
||||
*/
|
||||
private static function appendNumericBytes(string $content, BitArray $bits) : void
|
||||
{
|
||||
$length = strlen($content);
|
||||
$i = 0;
|
||||
|
||||
while ($i < $length) {
|
||||
$num1 = (int) $content[$i];
|
||||
|
||||
if ($i + 2 < $length) {
|
||||
// Encode three numeric letters in ten bits.
|
||||
$num2 = (int) $content[$i + 1];
|
||||
$num3 = (int) $content[$i + 2];
|
||||
$bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
|
||||
$i += 3;
|
||||
} elseif ($i + 1 < $length) {
|
||||
// Encode two numeric letters in seven bits.
|
||||
$num2 = (int) $content[$i + 1];
|
||||
$bits->appendBits($num1 * 10 + $num2, 7);
|
||||
$i += 2;
|
||||
} else {
|
||||
// Encode one numeric letter in four bits.
|
||||
$bits->appendBits($num1, 4);
|
||||
++$i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends alpha-numeric bytes to a bit array.
|
||||
*
|
||||
* @throws WriterException if an invalid alphanumeric code was found
|
||||
*/
|
||||
private static function appendAlphanumericBytes(string $content, BitArray $bits) : void
|
||||
{
|
||||
$length = strlen($content);
|
||||
$i = 0;
|
||||
|
||||
while ($i < $length) {
|
||||
$code1 = self::getAlphanumericCode(ord($content[$i]));
|
||||
|
||||
if (-1 === $code1) {
|
||||
throw new WriterException('Invalid alphanumeric code');
|
||||
}
|
||||
|
||||
if ($i + 1 < $length) {
|
||||
$code2 = self::getAlphanumericCode(ord($content[$i + 1]));
|
||||
|
||||
if (-1 === $code2) {
|
||||
throw new WriterException('Invalid alphanumeric code');
|
||||
}
|
||||
|
||||
// Encode two alphanumeric letters in 11 bits.
|
||||
$bits->appendBits($code1 * 45 + $code2, 11);
|
||||
$i += 2;
|
||||
} else {
|
||||
// Encode one alphanumeric letter in six bits.
|
||||
$bits->appendBits($code1, 6);
|
||||
++$i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends regular 8-bit bytes to a bit array.
|
||||
*
|
||||
* @throws WriterException if content cannot be encoded to target encoding
|
||||
*/
|
||||
private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void
|
||||
{
|
||||
$bytes = @iconv('utf-8', $encoding, $content);
|
||||
|
||||
if (false === $bytes) {
|
||||
throw new WriterException('Could not encode content to ' . $encoding);
|
||||
}
|
||||
|
||||
$length = strlen($bytes);
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$bits->appendBits(ord($bytes[$i]), 8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends KANJI bytes to a bit array.
|
||||
*
|
||||
* @throws WriterException if content does not seem to be encoded in SHIFT-JIS
|
||||
* @throws WriterException if an invalid byte sequence occurs
|
||||
*/
|
||||
private static function appendKanjiBytes(string $content, BitArray $bits) : void
|
||||
{
|
||||
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
|
||||
|
||||
if (false === $bytes) {
|
||||
throw new WriterException('Content could not be converted to SHIFT-JIS');
|
||||
}
|
||||
|
||||
if (strlen($bytes) % 2 > 0) {
|
||||
// We just do a simple length check here. The for loop will check
|
||||
// individual characters.
|
||||
throw new WriterException('Content does not seem to be encoded in SHIFT-JIS');
|
||||
}
|
||||
|
||||
$length = strlen($bytes);
|
||||
|
||||
for ($i = 0; $i < $length; $i += 2) {
|
||||
$byte1 = ord($bytes[$i]);
|
||||
$byte2 = ord($bytes[$i + 1]);
|
||||
$code = ($byte1 << 8) | $byte2;
|
||||
|
||||
if ($code >= 0x8140 && $code <= 0x9ffc) {
|
||||
$subtracted = $code - 0x8140;
|
||||
} elseif ($code >= 0xe040 && $code <= 0xebbf) {
|
||||
$subtracted = $code - 0xc140;
|
||||
} else {
|
||||
throw new WriterException('Invalid byte sequence');
|
||||
}
|
||||
|
||||
$encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff);
|
||||
|
||||
$bits->appendBits($encoded, 13);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends ECI information to a bit array.
|
||||
*/
|
||||
private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void
|
||||
{
|
||||
$mode = Mode::ECI();
|
||||
$bits->appendBits($mode->getBits(), 4);
|
||||
$bits->appendBits($eci->getValue(), 8);
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Encoder;
|
||||
|
||||
/**
|
||||
* Mask utility.
|
||||
*/
|
||||
final class MaskUtil
|
||||
{
|
||||
/**#@+
|
||||
* Penalty weights from section 6.8.2.1
|
||||
*/
|
||||
public const N1 = 3;
|
||||
public const N2 = 3;
|
||||
public const N3 = 40;
|
||||
public const N4 = 10;
|
||||
/**#@-*/
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies mask penalty rule 1 and returns the penalty.
|
||||
*
|
||||
* Finds repetitive cells with the same color and gives penalty to them.
|
||||
* Example: 00000 or 11111.
|
||||
*/
|
||||
public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int
|
||||
{
|
||||
return (
|
||||
self::applyMaskPenaltyRule1Internal($matrix, true)
|
||||
+ self::applyMaskPenaltyRule1Internal($matrix, false)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies mask penalty rule 2 and returns the penalty.
|
||||
*
|
||||
* Finds 2x2 blocks with the same color and gives penalty to them. This is
|
||||
* actually equivalent to the spec's rule, which is to find MxN blocks and
|
||||
* give a penalty proportional to (M-1)x(N-1), because this is the number of
|
||||
* 2x2 blocks inside such a block.
|
||||
*/
|
||||
public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int
|
||||
{
|
||||
$penalty = 0;
|
||||
$array = $matrix->getArray();
|
||||
$width = $matrix->getWidth();
|
||||
$height = $matrix->getHeight();
|
||||
|
||||
for ($y = 0; $y < $height - 1; ++$y) {
|
||||
for ($x = 0; $x < $width - 1; ++$x) {
|
||||
$value = $array[$y][$x];
|
||||
|
||||
if ($value === $array[$y][$x + 1]
|
||||
&& $value === $array[$y + 1][$x]
|
||||
&& $value === $array[$y + 1][$x + 1]
|
||||
) {
|
||||
++$penalty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::N2 * $penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies mask penalty rule 3 and returns the penalty.
|
||||
*
|
||||
* Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
|
||||
* to them. If we find patterns like 000010111010000, we give penalties
|
||||
* twice (i.e. 40 * 2).
|
||||
*/
|
||||
public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int
|
||||
{
|
||||
$penalty = 0;
|
||||
$array = $matrix->getArray();
|
||||
$width = $matrix->getWidth();
|
||||
$height = $matrix->getHeight();
|
||||
|
||||
for ($y = 0; $y < $height; ++$y) {
|
||||
for ($x = 0; $x < $width; ++$x) {
|
||||
if ($x + 6 < $width
|
||||
&& 1 === $array[$y][$x]
|
||||
&& 0 === $array[$y][$x + 1]
|
||||
&& 1 === $array[$y][$x + 2]
|
||||
&& 1 === $array[$y][$x + 3]
|
||||
&& 1 === $array[$y][$x + 4]
|
||||
&& 0 === $array[$y][$x + 5]
|
||||
&& 1 === $array[$y][$x + 6]
|
||||
&& (
|
||||
(
|
||||
$x + 10 < $width
|
||||
&& 0 === $array[$y][$x + 7]
|
||||
&& 0 === $array[$y][$x + 8]
|
||||
&& 0 === $array[$y][$x + 9]
|
||||
&& 0 === $array[$y][$x + 10]
|
||||
)
|
||||
|| (
|
||||
$x - 4 >= 0
|
||||
&& 0 === $array[$y][$x - 1]
|
||||
&& 0 === $array[$y][$x - 2]
|
||||
&& 0 === $array[$y][$x - 3]
|
||||
&& 0 === $array[$y][$x - 4]
|
||||
)
|
||||
)
|
||||
) {
|
||||
$penalty += self::N3;
|
||||
}
|
||||
|
||||
if ($y + 6 < $height
|
||||
&& 1 === $array[$y][$x]
|
||||
&& 0 === $array[$y + 1][$x]
|
||||
&& 1 === $array[$y + 2][$x]
|
||||
&& 1 === $array[$y + 3][$x]
|
||||
&& 1 === $array[$y + 4][$x]
|
||||
&& 0 === $array[$y + 5][$x]
|
||||
&& 1 === $array[$y + 6][$x]
|
||||
&& (
|
||||
(
|
||||
$y + 10 < $height
|
||||
&& 0 === $array[$y + 7][$x]
|
||||
&& 0 === $array[$y + 8][$x]
|
||||
&& 0 === $array[$y + 9][$x]
|
||||
&& 0 === $array[$y + 10][$x]
|
||||
)
|
||||
|| (
|
||||
$y - 4 >= 0
|
||||
&& 0 === $array[$y - 1][$x]
|
||||
&& 0 === $array[$y - 2][$x]
|
||||
&& 0 === $array[$y - 3][$x]
|
||||
&& 0 === $array[$y - 4][$x]
|
||||
)
|
||||
)
|
||||
) {
|
||||
$penalty += self::N3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies mask penalty rule 4 and returns the penalty.
|
||||
*
|
||||
* Calculates the ratio of dark cells and gives penalty if the ratio is far
|
||||
* from 50%. It gives 10 penalty for 5% distance.
|
||||
*/
|
||||
public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int
|
||||
{
|
||||
$numDarkCells = 0;
|
||||
|
||||
$array = $matrix->getArray();
|
||||
$width = $matrix->getWidth();
|
||||
$height = $matrix->getHeight();
|
||||
|
||||
for ($y = 0; $y < $height; ++$y) {
|
||||
$arrayY = $array[$y];
|
||||
|
||||
for ($x = 0; $x < $width; ++$x) {
|
||||
if (1 === $arrayY[$x]) {
|
||||
++$numDarkCells;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$numTotalCells = $height * $width;
|
||||
$darkRatio = $numDarkCells / $numTotalCells;
|
||||
$fixedPercentVariances = (int) floor(abs($darkRatio - 0.5) * 20);
|
||||
|
||||
return $fixedPercentVariances * self::N4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mask bit for "getMaskPattern" at "x" and "y".
|
||||
*
|
||||
* See 8.8 of JISX0510:2004 for mask pattern conditions.
|
||||
*/
|
||||
public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
|
||||
{
|
||||
return 0 === match ($maskPattern) {
|
||||
0 => ($x + $y) % 2,
|
||||
1 => $y % 2,
|
||||
2 => $x % 3,
|
||||
3 => ($x + $y) % 3,
|
||||
4 => (intdiv($y, 2) + intdiv($x, 3)) % 2,
|
||||
5 => (($x * $y) % 2) + (($x * $y) % 3),
|
||||
6 => ((($x * $y) % 2) + (($x * $y) % 3)) % 2,
|
||||
7 => ((($x + $y) % 2) + (($x * $y) % 3)) % 2,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for applyMaskPenaltyRule1.
|
||||
*
|
||||
* We need this for doing this calculation in both vertical and horizontal
|
||||
* orders respectively.
|
||||
*/
|
||||
private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int
|
||||
{
|
||||
$penalty = 0;
|
||||
$iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
|
||||
$jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
|
||||
$array = $matrix->getArray();
|
||||
|
||||
for ($i = 0; $i < $iLimit; ++$i) {
|
||||
$numSameBitCells = 0;
|
||||
$prevBit = -1;
|
||||
|
||||
for ($j = 0; $j < $jLimit; $j++) {
|
||||
$bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
|
||||
|
||||
if ($bit === $prevBit) {
|
||||
++$numSameBitCells;
|
||||
} else {
|
||||
if ($numSameBitCells >= 5) {
|
||||
$penalty += self::N1 + ($numSameBitCells - 5);
|
||||
}
|
||||
|
||||
$numSameBitCells = 1;
|
||||
$prevBit = $bit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($numSameBitCells >= 5) {
|
||||
$penalty += self::N1 + ($numSameBitCells - 5);
|
||||
}
|
||||
}
|
||||
|
||||
return $penalty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Encoder;
|
||||
|
||||
use BaconQrCode\Common\BitArray;
|
||||
use BaconQrCode\Common\ErrorCorrectionLevel;
|
||||
use BaconQrCode\Common\Version;
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Exception\WriterException;
|
||||
|
||||
/**
|
||||
* Matrix utility.
|
||||
*/
|
||||
final class MatrixUtil
|
||||
{
|
||||
/**
|
||||
* Position detection pattern.
|
||||
*/
|
||||
private const POSITION_DETECTION_PATTERN = [
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
[1, 0, 0, 0, 0, 0, 1],
|
||||
[1, 0, 1, 1, 1, 0, 1],
|
||||
[1, 0, 1, 1, 1, 0, 1],
|
||||
[1, 0, 1, 1, 1, 0, 1],
|
||||
[1, 0, 0, 0, 0, 0, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Position adjustment pattern.
|
||||
*/
|
||||
private const POSITION_ADJUSTMENT_PATTERN = [
|
||||
[1, 1, 1, 1, 1],
|
||||
[1, 0, 0, 0, 1],
|
||||
[1, 0, 1, 0, 1],
|
||||
[1, 0, 0, 0, 1],
|
||||
[1, 1, 1, 1, 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Coordinates for position adjustment patterns for each version.
|
||||
*/
|
||||
private const POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = [
|
||||
[null, null, null, null, null, null, null], // Version 1
|
||||
[ 6, 18, null, null, null, null, null], // Version 2
|
||||
[ 6, 22, null, null, null, null, null], // Version 3
|
||||
[ 6, 26, null, null, null, null, null], // Version 4
|
||||
[ 6, 30, null, null, null, null, null], // Version 5
|
||||
[ 6, 34, null, null, null, null, null], // Version 6
|
||||
[ 6, 22, 38, null, null, null, null], // Version 7
|
||||
[ 6, 24, 42, null, null, null, null], // Version 8
|
||||
[ 6, 26, 46, null, null, null, null], // Version 9
|
||||
[ 6, 28, 50, null, null, null, null], // Version 10
|
||||
[ 6, 30, 54, null, null, null, null], // Version 11
|
||||
[ 6, 32, 58, null, null, null, null], // Version 12
|
||||
[ 6, 34, 62, null, null, null, null], // Version 13
|
||||
[ 6, 26, 46, 66, null, null, null], // Version 14
|
||||
[ 6, 26, 48, 70, null, null, null], // Version 15
|
||||
[ 6, 26, 50, 74, null, null, null], // Version 16
|
||||
[ 6, 30, 54, 78, null, null, null], // Version 17
|
||||
[ 6, 30, 56, 82, null, null, null], // Version 18
|
||||
[ 6, 30, 58, 86, null, null, null], // Version 19
|
||||
[ 6, 34, 62, 90, null, null, null], // Version 20
|
||||
[ 6, 28, 50, 72, 94, null, null], // Version 21
|
||||
[ 6, 26, 50, 74, 98, null, null], // Version 22
|
||||
[ 6, 30, 54, 78, 102, null, null], // Version 23
|
||||
[ 6, 28, 54, 80, 106, null, null], // Version 24
|
||||
[ 6, 32, 58, 84, 110, null, null], // Version 25
|
||||
[ 6, 30, 58, 86, 114, null, null], // Version 26
|
||||
[ 6, 34, 62, 90, 118, null, null], // Version 27
|
||||
[ 6, 26, 50, 74, 98, 122, null], // Version 28
|
||||
[ 6, 30, 54, 78, 102, 126, null], // Version 29
|
||||
[ 6, 26, 52, 78, 104, 130, null], // Version 30
|
||||
[ 6, 30, 56, 82, 108, 134, null], // Version 31
|
||||
[ 6, 34, 60, 86, 112, 138, null], // Version 32
|
||||
[ 6, 30, 58, 86, 114, 142, null], // Version 33
|
||||
[ 6, 34, 62, 90, 118, 146, null], // Version 34
|
||||
[ 6, 30, 54, 78, 102, 126, 150], // Version 35
|
||||
[ 6, 24, 50, 76, 102, 128, 154], // Version 36
|
||||
[ 6, 28, 54, 80, 106, 132, 158], // Version 37
|
||||
[ 6, 32, 58, 84, 110, 136, 162], // Version 38
|
||||
[ 6, 26, 54, 82, 110, 138, 166], // Version 39
|
||||
[ 6, 30, 58, 86, 114, 142, 170], // Version 40
|
||||
];
|
||||
|
||||
/**
|
||||
* Type information coordinates.
|
||||
*/
|
||||
private const TYPE_INFO_COORDINATES = [
|
||||
[8, 0],
|
||||
[8, 1],
|
||||
[8, 2],
|
||||
[8, 3],
|
||||
[8, 4],
|
||||
[8, 5],
|
||||
[8, 7],
|
||||
[8, 8],
|
||||
[7, 8],
|
||||
[5, 8],
|
||||
[4, 8],
|
||||
[3, 8],
|
||||
[2, 8],
|
||||
[1, 8],
|
||||
[0, 8],
|
||||
];
|
||||
|
||||
/**
|
||||
* Version information polynomial.
|
||||
*/
|
||||
private const VERSION_INFO_POLY = 0x1f25;
|
||||
|
||||
/**
|
||||
* Type information polynomial.
|
||||
*/
|
||||
private const TYPE_INFO_POLY = 0x537;
|
||||
|
||||
/**
|
||||
* Type information mask pattern.
|
||||
*/
|
||||
private const TYPE_INFO_MASK_PATTERN = 0x5412;
|
||||
|
||||
/**
|
||||
* Clears a given matrix.
|
||||
*/
|
||||
public static function clearMatrix(ByteMatrix $matrix) : void
|
||||
{
|
||||
$matrix->clear(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a complete matrix.
|
||||
*/
|
||||
public static function buildMatrix(
|
||||
BitArray $dataBits,
|
||||
ErrorCorrectionLevel $level,
|
||||
Version $version,
|
||||
int $maskPattern,
|
||||
ByteMatrix $matrix
|
||||
) : void {
|
||||
self::clearMatrix($matrix);
|
||||
self::embedBasicPatterns($version, $matrix);
|
||||
self::embedTypeInfo($level, $maskPattern, $matrix);
|
||||
self::maybeEmbedVersionInfo($version, $matrix);
|
||||
self::embedDataBits($dataBits, $maskPattern, $matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the position detection patterns from a matrix.
|
||||
*
|
||||
* This can be useful if you need to render those patterns separately.
|
||||
*/
|
||||
public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void
|
||||
{
|
||||
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
|
||||
|
||||
self::removePositionDetectionPattern(0, 0, $matrix);
|
||||
self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
|
||||
self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds type information into a matrix.
|
||||
*/
|
||||
private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void
|
||||
{
|
||||
$typeInfoBits = new BitArray();
|
||||
self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits);
|
||||
|
||||
$typeInfoBitsSize = $typeInfoBits->getSize();
|
||||
|
||||
for ($i = 0; $i < $typeInfoBitsSize; ++$i) {
|
||||
$bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i);
|
||||
|
||||
$x1 = self::TYPE_INFO_COORDINATES[$i][0];
|
||||
$y1 = self::TYPE_INFO_COORDINATES[$i][1];
|
||||
|
||||
$matrix->set($x1, $y1, (int) $bit);
|
||||
|
||||
if ($i < 8) {
|
||||
$x2 = $matrix->getWidth() - $i - 1;
|
||||
$y2 = 8;
|
||||
} else {
|
||||
$x2 = 8;
|
||||
$y2 = $matrix->getHeight() - 7 + ($i - 8);
|
||||
}
|
||||
|
||||
$matrix->set($x2, $y2, (int) $bit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates type information bits and appends them to a bit array.
|
||||
*
|
||||
* @throws RuntimeException if bit array resulted in invalid size
|
||||
*/
|
||||
private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void
|
||||
{
|
||||
$typeInfo = ($level->getBits() << 3) | $maskPattern;
|
||||
$bits->appendBits($typeInfo, 5);
|
||||
|
||||
$bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY);
|
||||
$bits->appendBits($bchCode, 10);
|
||||
|
||||
$maskBits = new BitArray();
|
||||
$maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15);
|
||||
$bits->xorBits($maskBits);
|
||||
|
||||
if (15 !== $bits->getSize()) {
|
||||
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds version information if required.
|
||||
*/
|
||||
private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void
|
||||
{
|
||||
if ($version->getVersionNumber() < 7) {
|
||||
return;
|
||||
}
|
||||
|
||||
$versionInfoBits = new BitArray();
|
||||
self::makeVersionInfoBits($version, $versionInfoBits);
|
||||
|
||||
$bitIndex = 6 * 3 - 1;
|
||||
|
||||
for ($i = 0; $i < 6; ++$i) {
|
||||
for ($j = 0; $j < 3; ++$j) {
|
||||
$bit = $versionInfoBits->get($bitIndex);
|
||||
--$bitIndex;
|
||||
|
||||
$matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit);
|
||||
$matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates version information bits and appends them to a bit array.
|
||||
*
|
||||
* @throws RuntimeException if bit array resulted in invalid size
|
||||
*/
|
||||
private static function makeVersionInfoBits(Version $version, BitArray $bits) : void
|
||||
{
|
||||
$bits->appendBits($version->getVersionNumber(), 6);
|
||||
|
||||
$bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY);
|
||||
$bits->appendBits($bchCode, 12);
|
||||
|
||||
if (18 !== $bits->getSize()) {
|
||||
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the BCH code for a value and a polynomial.
|
||||
*/
|
||||
private static function calculateBchCode(int $value, int $poly) : int
|
||||
{
|
||||
$msbSetInPoly = self::findMsbSet($poly);
|
||||
$value <<= $msbSetInPoly - 1;
|
||||
|
||||
while (self::findMsbSet($value) >= $msbSetInPoly) {
|
||||
$value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and MSB set.
|
||||
*/
|
||||
private static function findMsbSet(int $value) : int
|
||||
{
|
||||
$numDigits = 0;
|
||||
|
||||
while (0 !== $value) {
|
||||
$value >>= 1;
|
||||
++$numDigits;
|
||||
}
|
||||
|
||||
return $numDigits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds basic patterns into a matrix.
|
||||
*/
|
||||
private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void
|
||||
{
|
||||
self::embedPositionDetectionPatternsAndSeparators($matrix);
|
||||
self::embedDarkDotAtLeftBottomCorner($matrix);
|
||||
self::maybeEmbedPositionAdjustmentPatterns($version, $matrix);
|
||||
self::embedTimingPatterns($matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds position detection patterns and separators into a byte matrix.
|
||||
*/
|
||||
private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void
|
||||
{
|
||||
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
|
||||
|
||||
self::embedPositionDetectionPattern(0, 0, $matrix);
|
||||
self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
|
||||
self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
|
||||
|
||||
$hspWidth = 8;
|
||||
|
||||
self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix);
|
||||
self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix);
|
||||
self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix);
|
||||
|
||||
$vspSize = 7;
|
||||
|
||||
self::embedVerticalSeparationPattern($vspSize, 0, $matrix);
|
||||
self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix);
|
||||
self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a single position detection pattern into a byte matrix.
|
||||
*/
|
||||
private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
|
||||
{
|
||||
for ($y = 0; $y < 7; ++$y) {
|
||||
for ($x = 0; $x < 7; ++$x) {
|
||||
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
|
||||
{
|
||||
for ($y = 0; $y < 7; ++$y) {
|
||||
for ($x = 0; $x < 7; ++$x) {
|
||||
$matrix->set($xStart + $x, $yStart + $y, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a single horizontal separation pattern.
|
||||
*
|
||||
* @throws RuntimeException if a byte was already set
|
||||
*/
|
||||
private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
|
||||
{
|
||||
for ($x = 0; $x < 8; $x++) {
|
||||
if (-1 !== $matrix->get($xStart + $x, $yStart)) {
|
||||
throw new RuntimeException('Byte already set');
|
||||
}
|
||||
|
||||
$matrix->set($xStart + $x, $yStart, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a single vertical separation pattern.
|
||||
*
|
||||
* @throws RuntimeException if a byte was already set
|
||||
*/
|
||||
private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
|
||||
{
|
||||
for ($y = 0; $y < 7; $y++) {
|
||||
if (-1 !== $matrix->get($xStart, $yStart + $y)) {
|
||||
throw new RuntimeException('Byte already set');
|
||||
}
|
||||
|
||||
$matrix->set($xStart, $yStart + $y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a dot at the left bottom corner.
|
||||
*
|
||||
* @throws RuntimeException if a byte was already set to 0
|
||||
*/
|
||||
private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void
|
||||
{
|
||||
if (0 === $matrix->get(8, $matrix->getHeight() - 8)) {
|
||||
throw new RuntimeException('Byte already set to 0');
|
||||
}
|
||||
|
||||
$matrix->set(8, $matrix->getHeight() - 8, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds position adjustment patterns if required.
|
||||
*/
|
||||
private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void
|
||||
{
|
||||
if ($version->getVersionNumber() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
$index = $version->getVersionNumber() - 1;
|
||||
|
||||
$coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index];
|
||||
$numCoordinates = count($coordinates);
|
||||
|
||||
for ($i = 0; $i < $numCoordinates; ++$i) {
|
||||
for ($j = 0; $j < $numCoordinates; ++$j) {
|
||||
$y = $coordinates[$i];
|
||||
$x = $coordinates[$j];
|
||||
|
||||
if (null === $x || null === $y) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (-1 === $matrix->get($x, $y)) {
|
||||
self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a single position adjustment pattern.
|
||||
*/
|
||||
private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
|
||||
{
|
||||
for ($y = 0; $y < 5; $y++) {
|
||||
for ($x = 0; $x < 5; $x++) {
|
||||
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds timing patterns into a matrix.
|
||||
*/
|
||||
private static function embedTimingPatterns(ByteMatrix $matrix) : void
|
||||
{
|
||||
$matrixWidth = $matrix->getWidth();
|
||||
|
||||
for ($i = 8; $i < $matrixWidth - 8; ++$i) {
|
||||
$bit = ($i + 1) % 2;
|
||||
|
||||
if (-1 === $matrix->get($i, 6)) {
|
||||
$matrix->set($i, 6, $bit);
|
||||
}
|
||||
|
||||
if (-1 === $matrix->get(6, $i)) {
|
||||
$matrix->set(6, $i, $bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds "dataBits" using "getMaskPattern".
|
||||
*
|
||||
* For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for
|
||||
* how to embed data bits.
|
||||
*
|
||||
* @throws WriterException if not all bits could be consumed
|
||||
*/
|
||||
private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void
|
||||
{
|
||||
$bitIndex = 0;
|
||||
$direction = -1;
|
||||
|
||||
// Start from the right bottom cell.
|
||||
$x = $matrix->getWidth() - 1;
|
||||
$y = $matrix->getHeight() - 1;
|
||||
|
||||
while ($x > 0) {
|
||||
// Skip vertical timing pattern.
|
||||
if (6 === $x) {
|
||||
--$x;
|
||||
}
|
||||
|
||||
while ($y >= 0 && $y < $matrix->getHeight()) {
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$xx = $x - $i;
|
||||
|
||||
// Skip the cell if it's not empty.
|
||||
if (-1 !== $matrix->get($xx, $y)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($bitIndex < $dataBits->getSize()) {
|
||||
$bit = $dataBits->get($bitIndex);
|
||||
++$bitIndex;
|
||||
} else {
|
||||
// Padding bit. If there is no bit left, we'll fill the
|
||||
// left cells with 0, as described in 8.4.9 of
|
||||
// JISX0510:2004 (p. 24).
|
||||
$bit = false;
|
||||
}
|
||||
|
||||
// Skip masking if maskPattern is -1.
|
||||
if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) {
|
||||
$bit = ! $bit;
|
||||
}
|
||||
|
||||
$matrix->set($xx, $y, (int) $bit);
|
||||
}
|
||||
|
||||
$y += $direction;
|
||||
}
|
||||
|
||||
$direction = -$direction;
|
||||
$y += $direction;
|
||||
$x -= 2;
|
||||
}
|
||||
|
||||
// All bits should be consumed
|
||||
if ($dataBits->getSize() !== $bitIndex) {
|
||||
throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')');
|
||||
}
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Encoder;
|
||||
|
||||
use BaconQrCode\Common\ErrorCorrectionLevel;
|
||||
use BaconQrCode\Common\Mode;
|
||||
use BaconQrCode\Common\Version;
|
||||
|
||||
/**
|
||||
* QR code.
|
||||
*/
|
||||
final class QrCode
|
||||
{
|
||||
/**
|
||||
* Number of possible mask patterns.
|
||||
*/
|
||||
public const NUM_MASK_PATTERNS = 8;
|
||||
|
||||
/**
|
||||
* Mask pattern of the QR code.
|
||||
*/
|
||||
private int $maskPattern = -1;
|
||||
|
||||
/**
|
||||
* Matrix of the QR code.
|
||||
*/
|
||||
private ByteMatrix $matrix;
|
||||
|
||||
public function __construct(
|
||||
private readonly Mode $mode,
|
||||
private readonly ErrorCorrectionLevel $errorCorrectionLevel,
|
||||
private readonly Version $version,
|
||||
int $maskPattern,
|
||||
ByteMatrix $matrix
|
||||
) {
|
||||
$this->maskPattern = $maskPattern;
|
||||
$this->matrix = $matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mode.
|
||||
*/
|
||||
public function getMode() : Mode
|
||||
{
|
||||
return $this->mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the EC level.
|
||||
*/
|
||||
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
|
||||
{
|
||||
return $this->errorCorrectionLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version.
|
||||
*/
|
||||
public function getVersion() : Version
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mask pattern.
|
||||
*/
|
||||
public function getMaskPattern() : int
|
||||
{
|
||||
return $this->maskPattern;
|
||||
}
|
||||
|
||||
public function getMatrix(): ByteMatrix
|
||||
{
|
||||
return $this->matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether a mask pattern is valid.
|
||||
*/
|
||||
public static function isValidMaskPattern(int $maskPattern) : bool
|
||||
{
|
||||
return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the QR code.
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
$result = "<<\n"
|
||||
. ' mode: ' . $this->mode . "\n"
|
||||
. ' ecLevel: ' . $this->errorCorrectionLevel . "\n"
|
||||
. ' version: ' . $this->version . "\n"
|
||||
. ' maskPattern: ' . $this->maskPattern . "\n";
|
||||
|
||||
if ($this->matrix === null) {
|
||||
$result .= " matrix: null\n";
|
||||
} else {
|
||||
$result .= " matrix:\n";
|
||||
$result .= $this->matrix;
|
||||
}
|
||||
|
||||
$result .= ">>\n";
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface ExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Exception;
|
||||
|
||||
final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Exception;
|
||||
|
||||
final class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Exception;
|
||||
|
||||
final class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Exception;
|
||||
|
||||
final class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Exception;
|
||||
|
||||
final class WriterException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Color;
|
||||
|
||||
use BaconQrCode\Exception;
|
||||
|
||||
final class Alpha implements ColorInterface
|
||||
{
|
||||
/**
|
||||
* @param int $alpha the alpha value, 0 to 100
|
||||
*/
|
||||
public function __construct(private readonly int $alpha, private readonly ColorInterface $baseColor)
|
||||
{
|
||||
if ($alpha < 0 || $alpha > 100) {
|
||||
throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100');
|
||||
}
|
||||
}
|
||||
|
||||
public function getAlpha() : int
|
||||
{
|
||||
return $this->alpha;
|
||||
}
|
||||
|
||||
public function getBaseColor() : ColorInterface
|
||||
{
|
||||
return $this->baseColor;
|
||||
}
|
||||
|
||||
public function toRgb() : Rgb
|
||||
{
|
||||
return $this->baseColor->toRgb();
|
||||
}
|
||||
|
||||
public function toCmyk() : Cmyk
|
||||
{
|
||||
return $this->baseColor->toCmyk();
|
||||
}
|
||||
|
||||
public function toGray() : Gray
|
||||
{
|
||||
return $this->baseColor->toGray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Color;
|
||||
|
||||
use BaconQrCode\Exception;
|
||||
|
||||
final class Cmyk implements ColorInterface
|
||||
{
|
||||
/**
|
||||
* @param int $cyan the cyan amount, 0 to 100
|
||||
* @param int $magenta the magenta amount, 0 to 100
|
||||
* @param int $yellow the yellow amount, 0 to 100
|
||||
* @param int $black the black amount, 0 to 100
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly int $cyan,
|
||||
private readonly int $magenta,
|
||||
private readonly int $yellow,
|
||||
private readonly int $black
|
||||
) {
|
||||
if ($cyan < 0 || $cyan > 100) {
|
||||
throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100');
|
||||
}
|
||||
|
||||
if ($magenta < 0 || $magenta > 100) {
|
||||
throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100');
|
||||
}
|
||||
|
||||
if ($yellow < 0 || $yellow > 100) {
|
||||
throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100');
|
||||
}
|
||||
|
||||
if ($black < 0 || $black > 100) {
|
||||
throw new Exception\InvalidArgumentException('Black must be between 0 and 100');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCyan() : int
|
||||
{
|
||||
return $this->cyan;
|
||||
}
|
||||
|
||||
public function getMagenta() : int
|
||||
{
|
||||
return $this->magenta;
|
||||
}
|
||||
|
||||
public function getYellow() : int
|
||||
{
|
||||
return $this->yellow;
|
||||
}
|
||||
|
||||
public function getBlack() : int
|
||||
{
|
||||
return $this->black;
|
||||
}
|
||||
|
||||
public function toRgb() : Rgb
|
||||
{
|
||||
$c = $this->cyan / 100;
|
||||
$m = $this->magenta / 100;
|
||||
$y = $this->yellow / 100;
|
||||
$k = $this->black / 100;
|
||||
|
||||
return new Rgb(
|
||||
(int) round(255 * (1 - $c) * (1 - $k)),
|
||||
(int) round(255 * (1 - $m) * (1 - $k)),
|
||||
(int) round(255 * (1 - $y) * (1 - $k))
|
||||
);
|
||||
}
|
||||
|
||||
public function toCmyk() : Cmyk
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toGray() : Gray
|
||||
{
|
||||
return $this->toRgb()->toGray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Color;
|
||||
|
||||
interface ColorInterface
|
||||
{
|
||||
/**
|
||||
* Converts the color to RGB.
|
||||
*/
|
||||
public function toRgb() : Rgb;
|
||||
|
||||
/**
|
||||
* Converts the color to CMYK.
|
||||
*/
|
||||
public function toCmyk() : Cmyk;
|
||||
|
||||
/**
|
||||
* Converts the color to gray.
|
||||
*/
|
||||
public function toGray() : Gray;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Color;
|
||||
|
||||
use BaconQrCode\Exception;
|
||||
|
||||
final class Gray implements ColorInterface
|
||||
{
|
||||
/**
|
||||
* @param int $gray the gray value between 0 (black) and 100 (white)
|
||||
*/
|
||||
public function __construct(private readonly int $gray)
|
||||
{
|
||||
if ($gray < 0 || $gray > 100) {
|
||||
throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
|
||||
}
|
||||
}
|
||||
|
||||
public function getGray() : int
|
||||
{
|
||||
return $this->gray;
|
||||
}
|
||||
|
||||
public function toRgb() : Rgb
|
||||
{
|
||||
// use 255/100 instead of 2.55 to avoid floating-point precision loss (100 * 2.55 = 254.999...)
|
||||
$value = (int) round($this->gray * 255 / 100);
|
||||
|
||||
return new Rgb($value, $value, $value);
|
||||
}
|
||||
|
||||
public function toCmyk() : Cmyk
|
||||
{
|
||||
return new Cmyk(0, 0, 0, 100 - $this->gray);
|
||||
}
|
||||
|
||||
public function toGray() : Gray
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Color;
|
||||
|
||||
use BaconQrCode\Exception;
|
||||
|
||||
final class Rgb implements ColorInterface
|
||||
{
|
||||
/**
|
||||
* @param int $red the red amount of the color, 0 to 255
|
||||
* @param int $green the green amount of the color, 0 to 255
|
||||
* @param int $blue the blue amount of the color, 0 to 255
|
||||
*/
|
||||
public function __construct(private readonly int $red, private readonly int $green, private readonly int $blue)
|
||||
{
|
||||
if ($red < 0 || $red > 255) {
|
||||
throw new Exception\InvalidArgumentException('Red must be between 0 and 255');
|
||||
}
|
||||
|
||||
if ($green < 0 || $green > 255) {
|
||||
throw new Exception\InvalidArgumentException('Green must be between 0 and 255');
|
||||
}
|
||||
|
||||
if ($blue < 0 || $blue > 255) {
|
||||
throw new Exception\InvalidArgumentException('Blue must be between 0 and 255');
|
||||
}
|
||||
}
|
||||
|
||||
public function getRed() : int
|
||||
{
|
||||
return $this->red;
|
||||
}
|
||||
|
||||
public function getGreen() : int
|
||||
{
|
||||
return $this->green;
|
||||
}
|
||||
|
||||
public function getBlue() : int
|
||||
{
|
||||
return $this->blue;
|
||||
}
|
||||
|
||||
public function toRgb() : Rgb
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toCmyk() : Cmyk
|
||||
{
|
||||
// avoid division by zero with input rgb(0,0,0), by handling it as a specific case
|
||||
if (0 === $this->red && 0 === $this->green && 0 === $this->blue) {
|
||||
return new Cmyk(0, 0, 0, 100);
|
||||
}
|
||||
|
||||
$c = 1 - ($this->red / 255);
|
||||
$m = 1 - ($this->green / 255);
|
||||
$y = 1 - ($this->blue / 255);
|
||||
$k = min($c, $m, $y);
|
||||
|
||||
return new Cmyk(
|
||||
(int) round(100 * ($c - $k) / (1 - $k)),
|
||||
(int) round(100 * ($m - $k) / (1 - $k)),
|
||||
(int) round(100 * ($y - $k) / (1 - $k)),
|
||||
(int) round(100 * $k)
|
||||
);
|
||||
}
|
||||
|
||||
public function toGray() : Gray
|
||||
{
|
||||
// use integer-based calculation to avoid floating-point precision loss
|
||||
return new Gray((int) round(($this->red * 2126 + $this->green * 7152 + $this->blue * 722) / 25500));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Eye;
|
||||
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
|
||||
/**
|
||||
* Combines the style of two different eyes.
|
||||
*/
|
||||
final class CompositeEye implements EyeInterface
|
||||
{
|
||||
public function __construct(private readonly EyeInterface $externalEye, private readonly EyeInterface $internalEye)
|
||||
{
|
||||
}
|
||||
|
||||
public function getExternalPath() : Path
|
||||
{
|
||||
return $this->externalEye->getExternalPath();
|
||||
}
|
||||
|
||||
public function getInternalPath() : Path
|
||||
{
|
||||
return $this->internalEye->getInternalPath();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Eye;
|
||||
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
|
||||
/**
|
||||
* Interface for describing the look of an eye.
|
||||
*/
|
||||
interface EyeInterface
|
||||
{
|
||||
/**
|
||||
* Returns the path of the external eye element.
|
||||
*
|
||||
* The path origin point (0, 0) must be anchored at the middle of the path.
|
||||
*/
|
||||
public function getExternalPath() : Path;
|
||||
|
||||
/**
|
||||
* Returns the path of the internal eye element.
|
||||
*
|
||||
* The path origin point (0, 0) must be anchored at the middle of the path.
|
||||
*/
|
||||
public function getInternalPath() : Path;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Eye;
|
||||
|
||||
use BaconQrCode\Encoder\ByteMatrix;
|
||||
use BaconQrCode\Renderer\Module\ModuleInterface;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
|
||||
/**
|
||||
* Renders an eye based on a module renderer.
|
||||
*/
|
||||
final class ModuleEye implements EyeInterface
|
||||
{
|
||||
public function __construct(private readonly ModuleInterface $module)
|
||||
{
|
||||
}
|
||||
|
||||
public function getExternalPath() : Path
|
||||
{
|
||||
$matrix = new ByteMatrix(7, 7);
|
||||
|
||||
for ($x = 0; $x < 7; ++$x) {
|
||||
$matrix->set($x, 0, 1);
|
||||
$matrix->set($x, 6, 1);
|
||||
}
|
||||
|
||||
for ($y = 1; $y < 6; ++$y) {
|
||||
$matrix->set(0, $y, 1);
|
||||
$matrix->set(6, $y, 1);
|
||||
}
|
||||
|
||||
return $this->module->createPath($matrix)->translate(-3.5, -3.5);
|
||||
}
|
||||
|
||||
public function getInternalPath() : Path
|
||||
{
|
||||
$matrix = new ByteMatrix(3, 3);
|
||||
|
||||
for ($x = 0; $x < 3; ++$x) {
|
||||
for ($y = 0; $y < 3; ++$y) {
|
||||
$matrix->set($x, $y, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->module->createPath($matrix)->translate(-1.5, -1.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Eye;
|
||||
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
|
||||
/**
|
||||
* Renders the outer eye as solid with a curved corner and inner eye as a circle.
|
||||
*/
|
||||
final class PointyEye implements EyeInterface
|
||||
{
|
||||
/**
|
||||
* @var self|null
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function instance() : self
|
||||
{
|
||||
return self::$instance ?: self::$instance = new self();
|
||||
}
|
||||
|
||||
public function getExternalPath() : Path
|
||||
{
|
||||
return (new Path())
|
||||
->move(-3.5, 3.5)
|
||||
->line(-3.5, 0)
|
||||
->ellipticArc(3.5, 3.5, 0, false, true, 0, -3.5)
|
||||
->line(3.5, -3.5)
|
||||
->line(3.5, 3.5)
|
||||
->close()
|
||||
->move(2.5, 0)
|
||||
->ellipticArc(2.5, 2.5, 0, false, true, 0, 2.5)
|
||||
->ellipticArc(2.5, 2.5, 0, false, true, -2.5, 0)
|
||||
->ellipticArc(2.5, 2.5, 0, false, true, 0, -2.5)
|
||||
->ellipticArc(2.5, 2.5, 0, false, true, 2.5, 0)
|
||||
->close()
|
||||
;
|
||||
}
|
||||
|
||||
public function getInternalPath() : Path
|
||||
{
|
||||
return (new Path())
|
||||
->move(1.5, 0)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
|
||||
->close()
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Eye;
|
||||
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
|
||||
/**
|
||||
* Renders the inner eye as a circle.
|
||||
*/
|
||||
final class SimpleCircleEye implements EyeInterface
|
||||
{
|
||||
private static ?SimpleCircleEye $instance = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function instance() : self
|
||||
{
|
||||
return self::$instance ?: self::$instance = new self();
|
||||
}
|
||||
|
||||
public function getExternalPath() : Path
|
||||
{
|
||||
return (new Path())
|
||||
->move(-3.5, -3.5)
|
||||
->line(3.5, -3.5)
|
||||
->line(3.5, 3.5)
|
||||
->line(-3.5, 3.5)
|
||||
->close()
|
||||
->move(-2.5, -2.5)
|
||||
->line(-2.5, 2.5)
|
||||
->line(2.5, 2.5)
|
||||
->line(2.5, -2.5)
|
||||
->close()
|
||||
;
|
||||
}
|
||||
|
||||
public function getInternalPath() : Path
|
||||
{
|
||||
return (new Path())
|
||||
->move(1.5, 0)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
|
||||
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
|
||||
->close()
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Eye;
|
||||
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
|
||||
/**
|
||||
* Renders the eyes in their default square shape.
|
||||
*/
|
||||
final class SquareEye implements EyeInterface
|
||||
{
|
||||
private static ?SquareEye $instance = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function instance() : self
|
||||
{
|
||||
return self::$instance ?: self::$instance = new self();
|
||||
}
|
||||
|
||||
public function getExternalPath() : Path
|
||||
{
|
||||
return (new Path())
|
||||
->move(-3.5, -3.5)
|
||||
->line(3.5, -3.5)
|
||||
->line(3.5, 3.5)
|
||||
->line(-3.5, 3.5)
|
||||
->close()
|
||||
->move(-2.5, -2.5)
|
||||
->line(-2.5, 2.5)
|
||||
->line(2.5, 2.5)
|
||||
->line(2.5, -2.5)
|
||||
->close()
|
||||
;
|
||||
}
|
||||
|
||||
public function getInternalPath() : Path
|
||||
{
|
||||
return (new Path())
|
||||
->move(-1.5, -1.5)
|
||||
->line(1.5, -1.5)
|
||||
->line(1.5, 1.5)
|
||||
->line(-1.5, 1.5)
|
||||
->close()
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BaconQrCode\Renderer;
|
||||
|
||||
use BaconQrCode\Encoder\ByteMatrix;
|
||||
use BaconQrCode\Encoder\MatrixUtil;
|
||||
use BaconQrCode\Encoder\QrCode;
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\Alpha;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\RendererStyle\EyeFill;
|
||||
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||
use GdImage;
|
||||
|
||||
final class GDLibRenderer implements RendererInterface
|
||||
{
|
||||
private ?GdImage $image;
|
||||
|
||||
/**
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $colors;
|
||||
|
||||
public function __construct(
|
||||
private int $size,
|
||||
private int $margin = 4,
|
||||
private string $imageFormat = 'png',
|
||||
private int $compressionQuality = 9,
|
||||
private ?Fill $fill = null
|
||||
) {
|
||||
if (! extension_loaded('gd') || ! function_exists('gd_info')) {
|
||||
throw new RuntimeException('You need to install the GD extension to use this back end');
|
||||
}
|
||||
|
||||
if ($this->fill === null) {
|
||||
$this->fill = Fill::default();
|
||||
}
|
||||
if ($this->fill->hasGradientFill()) {
|
||||
throw new InvalidArgumentException('GDLibRenderer does not support gradients');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if matrix width doesn't match height
|
||||
*/
|
||||
public function render(QrCode $qrCode): string
|
||||
{
|
||||
$matrix = $qrCode->getMatrix();
|
||||
$matrixSize = $matrix->getWidth();
|
||||
|
||||
if ($matrixSize !== $matrix->getHeight()) {
|
||||
throw new InvalidArgumentException('Matrix must have the same width and height');
|
||||
}
|
||||
|
||||
MatrixUtil::removePositionDetectionPatterns($matrix);
|
||||
$this->newImage();
|
||||
$this->draw($matrix);
|
||||
|
||||
return $this->renderImage();
|
||||
}
|
||||
|
||||
private function newImage(): void
|
||||
{
|
||||
$img = imagecreatetruecolor($this->size, $this->size);
|
||||
if ($img === false) {
|
||||
throw new RuntimeException('Failed to create image of that size');
|
||||
}
|
||||
|
||||
$this->image = $img;
|
||||
imagealphablending($this->image, false);
|
||||
imagesavealpha($this->image, true);
|
||||
|
||||
|
||||
$bg = $this->getColor($this->fill->getBackgroundColor());
|
||||
imagefilledrectangle($this->image, 0, 0, $this->size, $this->size, $bg);
|
||||
imagealphablending($this->image, true);
|
||||
}
|
||||
|
||||
private function draw(ByteMatrix $matrix): void
|
||||
{
|
||||
$matrixSize = $matrix->getWidth();
|
||||
|
||||
$pointsOnSide = $matrix->getWidth() + $this->margin * 2;
|
||||
$pointInPx = $this->size / $pointsOnSide;
|
||||
|
||||
$this->drawEye(0, 0, $pointInPx, $this->fill->getTopLeftEyeFill());
|
||||
$this->drawEye($matrixSize - 7, 0, $pointInPx, $this->fill->getTopRightEyeFill());
|
||||
$this->drawEye(0, $matrixSize - 7, $pointInPx, $this->fill->getBottomLeftEyeFill());
|
||||
|
||||
$rows = $matrix->getArray()->toArray();
|
||||
$color = $this->getColor($this->fill->getForegroundColor());
|
||||
for ($y = 0; $y < $matrixSize; $y += 1) {
|
||||
for ($x = 0; $x < $matrixSize; $x += 1) {
|
||||
if (! $rows[$y][$x]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$points = $this->normalizePoints([
|
||||
($this->margin + $x) * $pointInPx, ($this->margin + $y) * $pointInPx,
|
||||
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y) * $pointInPx,
|
||||
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
|
||||
($this->margin + $x) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
|
||||
]);
|
||||
imagefilledpolygon($this->image, $points, $color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function drawEye(int $xOffset, int $yOffset, float $pointInPx, EyeFill $eyeFill): void
|
||||
{
|
||||
$internalColor = $this->getColor($eyeFill->inheritsInternalColor()
|
||||
? $this->fill->getForegroundColor()
|
||||
: $eyeFill->getInternalColor());
|
||||
|
||||
$externalColor = $this->getColor($eyeFill->inheritsExternalColor()
|
||||
? $this->fill->getForegroundColor()
|
||||
: $eyeFill->getExternalColor());
|
||||
|
||||
for ($y = 0; $y < 7; $y += 1) {
|
||||
for ($x = 0; $x < 7; $x += 1) {
|
||||
if ((($y === 1 || $y === 5) && $x > 0 && $x < 6) || (($x === 1 || $x === 5) && $y > 0 && $y < 6)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$points = $this->normalizePoints([
|
||||
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
|
||||
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
|
||||
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
|
||||
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
|
||||
]);
|
||||
|
||||
if ($y > 1 && $y < 5 && $x > 1 && $x < 5) {
|
||||
imagefilledpolygon($this->image, $points, $internalColor);
|
||||
} else {
|
||||
imagefilledpolygon($this->image, $points, $externalColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize points will trim right and bottom line by 1 pixel.
|
||||
* Otherwise pixels of neighbors are overlapping which leads to issue with transparency and small QR codes.
|
||||
*/
|
||||
private function normalizePoints(array $points): array
|
||||
{
|
||||
$maxX = $maxY = 0;
|
||||
for ($i = 0; $i < count($points); $i += 2) {
|
||||
// Do manual round as GD just removes decimal part
|
||||
$points[$i] = $newX = round($points[$i]);
|
||||
$points[$i + 1] = $newY = round($points[$i + 1]);
|
||||
|
||||
$maxX = max($maxX, $newX);
|
||||
$maxY = max($maxY, $newY);
|
||||
}
|
||||
|
||||
// Do trimming only if there are 4 points (8 coordinates), assumes this is square.
|
||||
|
||||
for ($i = 0; $i < count($points); $i += 2) {
|
||||
$points[$i] = min($points[$i], $maxX - 1);
|
||||
$points[$i + 1] = min($points[$i + 1], $maxY - 1);
|
||||
}
|
||||
|
||||
return $points;
|
||||
}
|
||||
|
||||
private function renderImage(): string
|
||||
{
|
||||
ob_start();
|
||||
$quality = $this->compressionQuality;
|
||||
switch ($this->imageFormat) {
|
||||
case 'png':
|
||||
if ($quality > 9 || $quality < 0) {
|
||||
$quality = 9;
|
||||
}
|
||||
imagepng($this->image, null, $quality);
|
||||
break;
|
||||
|
||||
case 'gif':
|
||||
imagegif($this->image, null);
|
||||
break;
|
||||
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
if ($quality > 100 || $quality < 0) {
|
||||
$quality = 85;
|
||||
}
|
||||
imagejpeg($this->image, null, $quality);
|
||||
break;
|
||||
default:
|
||||
ob_end_clean();
|
||||
throw new InvalidArgumentException(
|
||||
'Supported image formats are jpeg, png and gif, got: ' . $this->imageFormat
|
||||
);
|
||||
}
|
||||
|
||||
$this->colors = [];
|
||||
$this->image = null;
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
private function getColor(ColorInterface $color): int
|
||||
{
|
||||
$alpha = 100;
|
||||
|
||||
if ($color instanceof Alpha) {
|
||||
$alpha = $color->getAlpha();
|
||||
$color = $color->getBaseColor();
|
||||
}
|
||||
|
||||
$rgb = $color->toRgb();
|
||||
|
||||
$colorKey = sprintf('%02X%02X%02X%02X', $rgb->getRed(), $rgb->getGreen(), $rgb->getBlue(), $alpha);
|
||||
|
||||
if (! isset($this->colors[$colorKey])) {
|
||||
$colorId = imagecolorallocatealpha(
|
||||
$this->image,
|
||||
$rgb->getRed(),
|
||||
$rgb->getGreen(),
|
||||
$rgb->getBlue(),
|
||||
(int)((100 - $alpha) / 100 * 127) // Alpha for GD is in range 0 (opaque) - 127 (transparent)
|
||||
);
|
||||
|
||||
if ($colorId === false) {
|
||||
throw new RuntimeException('Failed to create color: #' . $colorKey);
|
||||
}
|
||||
|
||||
$this->colors[$colorKey] = $colorId;
|
||||
}
|
||||
|
||||
return $this->colors[$colorKey];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\Alpha;
|
||||
use BaconQrCode\Renderer\Color\Cmyk;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Color\Gray;
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\Path\Close;
|
||||
use BaconQrCode\Renderer\Path\Curve;
|
||||
use BaconQrCode\Renderer\Path\EllipticArc;
|
||||
use BaconQrCode\Renderer\Path\Line;
|
||||
use BaconQrCode\Renderer\Path\Move;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
use BaconQrCode\Renderer\RendererStyle\GradientType;
|
||||
|
||||
final class EpsImageBackEnd implements ImageBackEndInterface
|
||||
{
|
||||
private const PRECISION = 3;
|
||||
|
||||
private ?string $eps;
|
||||
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void
|
||||
{
|
||||
$this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
|
||||
. "%%Creator: BaconQrCode\n"
|
||||
. sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
|
||||
. "%%BeginProlog\n"
|
||||
. "save\n"
|
||||
. "50 dict begin\n"
|
||||
. "/q { gsave } bind def\n"
|
||||
. "/Q { grestore } bind def\n"
|
||||
. "/s { scale } bind def\n"
|
||||
. "/t { translate } bind def\n"
|
||||
. "/r { rotate } bind def\n"
|
||||
. "/n { newpath } bind def\n"
|
||||
. "/m { moveto } bind def\n"
|
||||
. "/l { lineto } bind def\n"
|
||||
. "/c { curveto } bind def\n"
|
||||
. "/z { closepath } bind def\n"
|
||||
. "/f { eofill } bind def\n"
|
||||
. "/rgb { setrgbcolor } bind def\n"
|
||||
. "/cmyk { setcmykcolor } bind def\n"
|
||||
. "/gray { setgray } bind def\n"
|
||||
. "%%EndProlog\n"
|
||||
. "1 -1 s\n"
|
||||
. sprintf("0 -%d t\n", $size);
|
||||
|
||||
if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->eps .= wordwrap(
|
||||
'0 0 m'
|
||||
. sprintf(' %s 0 l', (string) $size)
|
||||
. sprintf(' %s %s l', (string) $size, (string) $size)
|
||||
. sprintf(' 0 %s l', (string) $size)
|
||||
. ' z'
|
||||
. ' ' .$this->getColorSetString($backgroundColor) . " f\n",
|
||||
75,
|
||||
"\n "
|
||||
);
|
||||
}
|
||||
|
||||
public function scale(float $size) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
|
||||
}
|
||||
|
||||
public function translate(float $x, float $y) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
|
||||
}
|
||||
|
||||
public function rotate(int $degrees) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= sprintf("%d r\n", $degrees);
|
||||
}
|
||||
|
||||
public function push() : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= "q\n";
|
||||
}
|
||||
|
||||
public function pop() : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= "Q\n";
|
||||
}
|
||||
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$fromX = 0;
|
||||
$fromY = 0;
|
||||
$this->eps .= wordwrap(
|
||||
'n '
|
||||
. $this->drawPathOperations($path, $fromX, $fromY)
|
||||
. ' ' . $this->getColorSetString($color) . " f\n",
|
||||
75,
|
||||
"\n "
|
||||
);
|
||||
}
|
||||
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void {
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$fromX = 0;
|
||||
$fromY = 0;
|
||||
$this->eps .= wordwrap(
|
||||
'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
|
||||
75,
|
||||
"\n "
|
||||
);
|
||||
|
||||
$this->createGradientFill($gradient, $x, $y, $width, $height);
|
||||
}
|
||||
|
||||
public function done() : string
|
||||
{
|
||||
if (null === $this->eps) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->eps .= "%%TRAILER\nend restore\n%%EOF";
|
||||
$blob = $this->eps;
|
||||
$this->eps = null;
|
||||
|
||||
return $blob;
|
||||
}
|
||||
|
||||
private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
|
||||
{
|
||||
$pathData = [];
|
||||
|
||||
foreach ($ops as $op) {
|
||||
switch (true) {
|
||||
case $op instanceof Move:
|
||||
$fromX = $toX = round($op->getX(), self::PRECISION);
|
||||
$fromY = $toY = round($op->getY(), self::PRECISION);
|
||||
$pathData[] = sprintf('%s %s m', $toX, $toY);
|
||||
break;
|
||||
|
||||
case $op instanceof Line:
|
||||
$fromX = $toX = round($op->getX(), self::PRECISION);
|
||||
$fromY = $toY = round($op->getY(), self::PRECISION);
|
||||
$pathData[] = sprintf('%s %s l', $toX, $toY);
|
||||
break;
|
||||
|
||||
case $op instanceof EllipticArc:
|
||||
$pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
|
||||
break;
|
||||
|
||||
case $op instanceof Curve:
|
||||
$x1 = round($op->getX1(), self::PRECISION);
|
||||
$y1 = round($op->getY1(), self::PRECISION);
|
||||
$x2 = round($op->getX2(), self::PRECISION);
|
||||
$y2 = round($op->getY2(), self::PRECISION);
|
||||
$fromX = $x3 = round($op->getX3(), self::PRECISION);
|
||||
$fromY = $y3 = round($op->getY3(), self::PRECISION);
|
||||
$pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
|
||||
break;
|
||||
|
||||
case $op instanceof Close:
|
||||
$pathData[] = 'z';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $pathData);
|
||||
}
|
||||
|
||||
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
|
||||
{
|
||||
$startColor = $gradient->getStartColor();
|
||||
$endColor = $gradient->getEndColor();
|
||||
|
||||
if ($startColor instanceof Alpha) {
|
||||
$startColor = $startColor->getBaseColor();
|
||||
}
|
||||
|
||||
$startColorType = get_class($startColor);
|
||||
|
||||
if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
|
||||
$startColorType = Cmyk::class;
|
||||
$startColor = $startColor->toCmyk();
|
||||
}
|
||||
|
||||
if (get_class($endColor) !== $startColorType) {
|
||||
switch ($startColorType) {
|
||||
case Cmyk::class:
|
||||
$endColor = $endColor->toCmyk();
|
||||
break;
|
||||
|
||||
case Rgb::class:
|
||||
$endColor = $endColor->toRgb();
|
||||
break;
|
||||
|
||||
case Gray::class:
|
||||
$endColor = $endColor->toGray();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->eps .= "eoclip\n<<\n";
|
||||
|
||||
if ($gradient->getType() === GradientType::RADIAL()) {
|
||||
$this->eps .= " /ShadingType 3\n";
|
||||
} else {
|
||||
$this->eps .= " /ShadingType 2\n";
|
||||
}
|
||||
|
||||
$this->eps .= " /Extend [ true true ]\n"
|
||||
. " /AntiAlias true\n";
|
||||
|
||||
switch ($startColorType) {
|
||||
case Cmyk::class:
|
||||
$this->eps .= " /ColorSpace /DeviceCMYK\n";
|
||||
break;
|
||||
|
||||
case Rgb::class:
|
||||
$this->eps .= " /ColorSpace /DeviceRGB\n";
|
||||
break;
|
||||
|
||||
case Gray::class:
|
||||
$this->eps .= " /ColorSpace /DeviceGray\n";
|
||||
break;
|
||||
}
|
||||
|
||||
switch ($gradient->getType()) {
|
||||
case GradientType::HORIZONTAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y, self::PRECISION),
|
||||
round($x + $width, self::PRECISION),
|
||||
round($y, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::VERTICAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y, self::PRECISION),
|
||||
round($x, self::PRECISION),
|
||||
round($y + $height, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::DIAGONAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y, self::PRECISION),
|
||||
round($x + $width, self::PRECISION),
|
||||
round($y + $height, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::INVERSE_DIAGONAL():
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s %s %s ]\n",
|
||||
round($x, self::PRECISION),
|
||||
round($y + $height, self::PRECISION),
|
||||
round($x + $width, self::PRECISION),
|
||||
round($y, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::RADIAL():
|
||||
$centerX = ($x + $width) / 2;
|
||||
$centerY = ($y + $height) / 2;
|
||||
|
||||
$this->eps .= sprintf(
|
||||
" /Coords [ %s %s 0 %s %s %s ]\n",
|
||||
round($centerX, self::PRECISION),
|
||||
round($centerY, self::PRECISION),
|
||||
round($centerX, self::PRECISION),
|
||||
round($centerY, self::PRECISION),
|
||||
round(max($width, $height) / 2, self::PRECISION)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
$this->eps .= " /Function\n"
|
||||
. " <<\n"
|
||||
. " /FunctionType 2\n"
|
||||
. " /Domain [ 0 1 ]\n"
|
||||
. sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor))
|
||||
. sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor))
|
||||
. " /N 1\n"
|
||||
. " >>\n>>\nshfill\nQ\n";
|
||||
}
|
||||
|
||||
private function getColorSetString(ColorInterface $color) : string
|
||||
{
|
||||
if ($color instanceof Rgb) {
|
||||
return $this->getColorString($color) . ' rgb';
|
||||
}
|
||||
|
||||
if ($color instanceof Cmyk) {
|
||||
return $this->getColorString($color) . ' cmyk';
|
||||
}
|
||||
|
||||
if ($color instanceof Gray) {
|
||||
return $this->getColorString($color) . ' gray';
|
||||
}
|
||||
|
||||
return $this->getColorSetString($color->toCmyk());
|
||||
}
|
||||
|
||||
private function getColorString(ColorInterface $color) : string
|
||||
{
|
||||
if ($color instanceof Rgb) {
|
||||
return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
|
||||
}
|
||||
|
||||
if ($color instanceof Cmyk) {
|
||||
return sprintf(
|
||||
'%s %s %s %s',
|
||||
$color->getCyan() / 100,
|
||||
$color->getMagenta() / 100,
|
||||
$color->getYellow() / 100,
|
||||
$color->getBlack() / 100
|
||||
);
|
||||
}
|
||||
|
||||
if ($color instanceof Gray) {
|
||||
return sprintf('%s', $color->getGray() / 100);
|
||||
}
|
||||
|
||||
return $this->getColorString($color->toCmyk());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
|
||||
/**
|
||||
* Interface for back ends able to to produce path based images.
|
||||
*/
|
||||
interface ImageBackEndInterface
|
||||
{
|
||||
/**
|
||||
* Starts a new image.
|
||||
*
|
||||
* If a previous image was already started, previous data get erased.
|
||||
*/
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void;
|
||||
|
||||
/**
|
||||
* Transforms all following drawing operation coordinates by scaling them by a given factor.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function scale(float $size) : void;
|
||||
|
||||
/**
|
||||
* Transforms all following drawing operation coordinates by translating them by a given amount.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function translate(float $x, float $y) : void;
|
||||
|
||||
/**
|
||||
* Transforms all following drawing operation coordinates by rotating them by a given amount.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function rotate(int $degrees) : void;
|
||||
|
||||
/**
|
||||
* Pushes the current coordinate transformation onto a stack.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function push() : void;
|
||||
|
||||
/**
|
||||
* Pops the last coordinate transformation from a stack.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function pop() : void;
|
||||
|
||||
/**
|
||||
* Draws a path with a given color.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void;
|
||||
|
||||
/**
|
||||
* Draws a path with a given gradient which spans the box described by the position and size.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void;
|
||||
|
||||
/**
|
||||
* Ends the image drawing operation and returns the resulting blob.
|
||||
*
|
||||
* This should reset the state of the back end and thus this method should only be callable once per image.
|
||||
*
|
||||
* @throws RuntimeException if no image was started yet.
|
||||
*/
|
||||
public function done() : string;
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\Alpha;
|
||||
use BaconQrCode\Renderer\Color\Cmyk;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Color\Gray;
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\Path\Close;
|
||||
use BaconQrCode\Renderer\Path\Curve;
|
||||
use BaconQrCode\Renderer\Path\EllipticArc;
|
||||
use BaconQrCode\Renderer\Path\Line;
|
||||
use BaconQrCode\Renderer\Path\Move;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
use BaconQrCode\Renderer\RendererStyle\GradientType;
|
||||
use Imagick;
|
||||
use ImagickDraw;
|
||||
use ImagickPixel;
|
||||
|
||||
final class ImagickImageBackEnd implements ImageBackEndInterface
|
||||
{
|
||||
private string $imageFormat;
|
||||
|
||||
private int $compressionQuality;
|
||||
|
||||
private ?Imagick $image;
|
||||
|
||||
private ?ImagickDraw $draw;
|
||||
|
||||
private ?int $gradientCount;
|
||||
|
||||
/**
|
||||
* @var TransformationMatrix[]|null
|
||||
*/
|
||||
private ?array $matrices;
|
||||
|
||||
private ?int $matrixIndex;
|
||||
|
||||
private bool $antialias;
|
||||
|
||||
public function __construct(string $imageFormat = 'png', int $compressionQuality = 100, bool $antialias = true)
|
||||
{
|
||||
if (! class_exists(Imagick::class)) {
|
||||
throw new RuntimeException('You need to install the imagick extension to use this back end');
|
||||
}
|
||||
|
||||
$this->imageFormat = $imageFormat;
|
||||
$this->compressionQuality = $compressionQuality;
|
||||
$this->antialias = $antialias;
|
||||
}
|
||||
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void
|
||||
{
|
||||
$this->image = new Imagick();
|
||||
$this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
|
||||
$this->image->setImageFormat($this->imageFormat);
|
||||
$this->image->setCompressionQuality($this->compressionQuality);
|
||||
$this->draw = new ImagickDraw();
|
||||
|
||||
if (! $this->antialias) {
|
||||
$this->image->setAntiAlias(false);
|
||||
$this->draw->setStrokeAntialias(false);
|
||||
}
|
||||
|
||||
$this->gradientCount = 0;
|
||||
$this->matrices = [new TransformationMatrix()];
|
||||
$this->matrixIndex = 0;
|
||||
}
|
||||
|
||||
public function scale(float $size) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->scale($size, $size);
|
||||
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
|
||||
->multiply(TransformationMatrix::scale($size));
|
||||
}
|
||||
|
||||
public function translate(float $x, float $y) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->translate($x, $y);
|
||||
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
|
||||
->multiply(TransformationMatrix::translate($x, $y));
|
||||
}
|
||||
|
||||
public function rotate(int $degrees) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->rotate($degrees);
|
||||
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
|
||||
->multiply(TransformationMatrix::rotate($degrees));
|
||||
}
|
||||
|
||||
public function push() : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->push();
|
||||
$this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
|
||||
}
|
||||
|
||||
public function pop() : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->pop();
|
||||
unset($this->matrices[$this->matrixIndex--]);
|
||||
}
|
||||
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->setFillColor($this->getColorPixel($color));
|
||||
$this->drawPath($path);
|
||||
}
|
||||
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void {
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
|
||||
$this->drawPath($path);
|
||||
}
|
||||
|
||||
public function done() : string
|
||||
{
|
||||
if (null === $this->draw) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->image->drawImage($this->draw);
|
||||
$blob = $this->image->getImageBlob();
|
||||
$this->draw->clear();
|
||||
$this->image->clear();
|
||||
$this->draw = null;
|
||||
$this->image = null;
|
||||
$this->gradientCount = null;
|
||||
|
||||
return $blob;
|
||||
}
|
||||
|
||||
private function drawPath(Path $path) : void
|
||||
{
|
||||
$this->draw->pathStart();
|
||||
|
||||
foreach ($path as $op) {
|
||||
switch (true) {
|
||||
case $op instanceof Move:
|
||||
$this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
|
||||
break;
|
||||
|
||||
case $op instanceof Line:
|
||||
$this->draw->pathLineToAbsolute($op->getX(), $op->getY());
|
||||
break;
|
||||
|
||||
case $op instanceof EllipticArc:
|
||||
$this->draw->pathEllipticArcAbsolute(
|
||||
$op->getXRadius(),
|
||||
$op->getYRadius(),
|
||||
$op->getXAxisAngle(),
|
||||
$op->isLargeArc(),
|
||||
$op->isSweep(),
|
||||
$op->getX(),
|
||||
$op->getY()
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Curve:
|
||||
$this->draw->pathCurveToAbsolute(
|
||||
$op->getX1(),
|
||||
$op->getY1(),
|
||||
$op->getX2(),
|
||||
$op->getY2(),
|
||||
$op->getX3(),
|
||||
$op->getY3()
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Close:
|
||||
$this->draw->pathClose();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
|
||||
}
|
||||
}
|
||||
|
||||
$this->draw->pathFinish();
|
||||
}
|
||||
|
||||
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
|
||||
{
|
||||
list($width, $height) = $this->matrices[$this->matrixIndex]->apply($width, $height);
|
||||
|
||||
$startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
|
||||
$endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
|
||||
$gradientImage = new Imagick();
|
||||
|
||||
switch ($gradient->getType()) {
|
||||
case GradientType::HORIZONTAL():
|
||||
$gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
|
||||
'gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
$gradientImage->rotateImage('transparent', -90);
|
||||
break;
|
||||
|
||||
case GradientType::VERTICAL():
|
||||
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
|
||||
'gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
break;
|
||||
|
||||
case GradientType::DIAGONAL():
|
||||
case GradientType::INVERSE_DIAGONAL():
|
||||
$gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
|
||||
'gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
|
||||
if (GradientType::DIAGONAL() === $gradient->getType()) {
|
||||
$gradientImage->rotateImage('transparent', -45);
|
||||
} else {
|
||||
$gradientImage->rotateImage('transparent', -135);
|
||||
}
|
||||
|
||||
$rotatedWidth = $gradientImage->getImageWidth();
|
||||
$rotatedHeight = $gradientImage->getImageHeight();
|
||||
|
||||
$gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
|
||||
$gradientImage->cropImage(
|
||||
intdiv($rotatedWidth, 2) - 2,
|
||||
intdiv($rotatedHeight, 2) - 2,
|
||||
intdiv($rotatedWidth, 4) + 1,
|
||||
intdiv($rotatedWidth, 4) + 1
|
||||
);
|
||||
break;
|
||||
|
||||
case GradientType::RADIAL():
|
||||
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
|
||||
'radial-gradient:%s-%s',
|
||||
$startColor,
|
||||
$endColor
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
$id = sprintf('g%d', ++$this->gradientCount);
|
||||
$this->draw->pushPattern($id, 0, 0, $width, $height);
|
||||
$this->draw->composite(Imagick::COMPOSITE_COPY, 0, 0, $width, $height, $gradientImage);
|
||||
$this->draw->popPattern();
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function getColorPixel(ColorInterface $color) : ImagickPixel
|
||||
{
|
||||
$alpha = 100;
|
||||
|
||||
if ($color instanceof Alpha) {
|
||||
$alpha = $color->getAlpha();
|
||||
$color = $color->getBaseColor();
|
||||
}
|
||||
|
||||
if ($color instanceof Rgb) {
|
||||
return new ImagickPixel(sprintf(
|
||||
'rgba(%d, %d, %d, %F)',
|
||||
$color->getRed(),
|
||||
$color->getGreen(),
|
||||
$color->getBlue(),
|
||||
$alpha / 100
|
||||
));
|
||||
}
|
||||
|
||||
if ($color instanceof Cmyk) {
|
||||
return new ImagickPixel(sprintf(
|
||||
'cmyka(%d, %d, %d, %d, %F)',
|
||||
$color->getCyan(),
|
||||
$color->getMagenta(),
|
||||
$color->getYellow(),
|
||||
$color->getBlack(),
|
||||
$alpha / 100
|
||||
));
|
||||
}
|
||||
|
||||
if ($color instanceof Gray) {
|
||||
return new ImagickPixel(sprintf(
|
||||
'graya(%d%%, %F)',
|
||||
$color->getGray(),
|
||||
$alpha / 100
|
||||
));
|
||||
}
|
||||
|
||||
return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
use BaconQrCode\Exception\RuntimeException;
|
||||
use BaconQrCode\Renderer\Color\Alpha;
|
||||
use BaconQrCode\Renderer\Color\ColorInterface;
|
||||
use BaconQrCode\Renderer\Path\Close;
|
||||
use BaconQrCode\Renderer\Path\Curve;
|
||||
use BaconQrCode\Renderer\Path\EllipticArc;
|
||||
use BaconQrCode\Renderer\Path\Line;
|
||||
use BaconQrCode\Renderer\Path\Move;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\Gradient;
|
||||
use BaconQrCode\Renderer\RendererStyle\GradientType;
|
||||
use XMLWriter;
|
||||
|
||||
final class SvgImageBackEnd implements ImageBackEndInterface
|
||||
{
|
||||
private const PRECISION = 3;
|
||||
|
||||
private ?XMLWriter $xmlWriter;
|
||||
|
||||
private ?array $stack;
|
||||
|
||||
private ?int $currentStack;
|
||||
|
||||
private ?int $gradientCount;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (! class_exists(XMLWriter::class)) {
|
||||
throw new RuntimeException(
|
||||
'You need to install the libxml extension and enable the xmlwriter extension to use this back end'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function new(int $size, ColorInterface $backgroundColor) : void
|
||||
{
|
||||
$this->xmlWriter = new XMLWriter();
|
||||
$this->xmlWriter->openMemory();
|
||||
|
||||
$this->xmlWriter->startDocument('1.0', 'UTF-8');
|
||||
$this->xmlWriter->startElement('svg');
|
||||
$this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
$this->xmlWriter->writeAttribute('version', '1.1');
|
||||
$this->xmlWriter->writeAttribute('width', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('height', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
|
||||
|
||||
$this->gradientCount = 0;
|
||||
$this->currentStack = 0;
|
||||
$this->stack[0] = 0;
|
||||
|
||||
$alpha = 1;
|
||||
|
||||
if ($backgroundColor instanceof Alpha) {
|
||||
$alpha = $backgroundColor->getAlpha() / 100;
|
||||
}
|
||||
|
||||
if (0 === $alpha) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('rect');
|
||||
$this->xmlWriter->writeAttribute('x', '0');
|
||||
$this->xmlWriter->writeAttribute('y', '0');
|
||||
$this->xmlWriter->writeAttribute('width', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('height', (string) $size);
|
||||
$this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
|
||||
|
||||
if ($alpha < 1) {
|
||||
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
public function scale(float $size) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->xmlWriter->writeAttribute(
|
||||
'transform',
|
||||
sprintf('scale(%s)', round($size, self::PRECISION))
|
||||
);
|
||||
++$this->stack[$this->currentStack];
|
||||
}
|
||||
|
||||
public function translate(float $x, float $y) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->xmlWriter->writeAttribute(
|
||||
'transform',
|
||||
sprintf('translate(%s,%s)', round($x, self::PRECISION), round($y, self::PRECISION))
|
||||
);
|
||||
++$this->stack[$this->currentStack];
|
||||
}
|
||||
|
||||
public function rotate(int $degrees) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
|
||||
++$this->stack[$this->currentStack];
|
||||
}
|
||||
|
||||
public function push() : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('g');
|
||||
$this->stack[] = 1;
|
||||
++$this->currentStack;
|
||||
}
|
||||
|
||||
public function pop() : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
array_pop($this->stack);
|
||||
--$this->currentStack;
|
||||
}
|
||||
|
||||
public function drawPathWithColor(Path $path, ColorInterface $color) : void
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$alpha = 1;
|
||||
|
||||
if ($color instanceof Alpha) {
|
||||
$alpha = $color->getAlpha() / 100;
|
||||
}
|
||||
|
||||
$this->startPathElement($path);
|
||||
$this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
|
||||
|
||||
if ($alpha < 1) {
|
||||
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
public function drawPathWithGradient(
|
||||
Path $path,
|
||||
Gradient $gradient,
|
||||
float $x,
|
||||
float $y,
|
||||
float $width,
|
||||
float $height
|
||||
) : void {
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
$gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
|
||||
$this->startPathElement($path);
|
||||
$this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
|
||||
public function done() : string
|
||||
{
|
||||
if (null === $this->xmlWriter) {
|
||||
throw new RuntimeException('No image has been started');
|
||||
}
|
||||
|
||||
foreach ($this->stack as $openElements) {
|
||||
for ($i = $openElements; $i > 0; --$i) {
|
||||
$this->xmlWriter->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
$this->xmlWriter->endDocument();
|
||||
$blob = $this->xmlWriter->outputMemory(true);
|
||||
$this->xmlWriter = null;
|
||||
$this->stack = null;
|
||||
$this->currentStack = null;
|
||||
$this->gradientCount = null;
|
||||
|
||||
return $blob;
|
||||
}
|
||||
|
||||
private function startPathElement(Path $path) : void
|
||||
{
|
||||
$pathData = [];
|
||||
|
||||
foreach ($path as $op) {
|
||||
switch (true) {
|
||||
case $op instanceof Move:
|
||||
$pathData[] = sprintf(
|
||||
'M%s %s',
|
||||
round($op->getX(), self::PRECISION),
|
||||
round($op->getY(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Line:
|
||||
$pathData[] = sprintf(
|
||||
'L%s %s',
|
||||
round($op->getX(), self::PRECISION),
|
||||
round($op->getY(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof EllipticArc:
|
||||
$pathData[] = sprintf(
|
||||
'A%s %s %s %u %u %s %s',
|
||||
round($op->getXRadius(), self::PRECISION),
|
||||
round($op->getYRadius(), self::PRECISION),
|
||||
round($op->getXAxisAngle(), self::PRECISION),
|
||||
$op->isLargeArc(),
|
||||
$op->isSweep(),
|
||||
round($op->getX(), self::PRECISION),
|
||||
round($op->getY(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Curve:
|
||||
$pathData[] = sprintf(
|
||||
'C%s %s %s %s %s %s',
|
||||
round($op->getX1(), self::PRECISION),
|
||||
round($op->getY1(), self::PRECISION),
|
||||
round($op->getX2(), self::PRECISION),
|
||||
round($op->getY2(), self::PRECISION),
|
||||
round($op->getX3(), self::PRECISION),
|
||||
round($op->getY3(), self::PRECISION)
|
||||
);
|
||||
break;
|
||||
|
||||
case $op instanceof Close:
|
||||
$pathData[] = 'Z';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
|
||||
}
|
||||
}
|
||||
|
||||
$this->xmlWriter->startElement('path');
|
||||
$this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
|
||||
$this->xmlWriter->writeAttribute('d', implode('', $pathData));
|
||||
}
|
||||
|
||||
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
|
||||
{
|
||||
$this->xmlWriter->startElement('defs');
|
||||
|
||||
$startColor = $gradient->getStartColor();
|
||||
$endColor = $gradient->getEndColor();
|
||||
|
||||
if ($gradient->getType() === GradientType::RADIAL()) {
|
||||
$this->xmlWriter->startElement('radialGradient');
|
||||
} else {
|
||||
$this->xmlWriter->startElement('linearGradient');
|
||||
}
|
||||
|
||||
$this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
|
||||
|
||||
switch ($gradient->getType()) {
|
||||
case GradientType::HORIZONTAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::VERTICAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::DIAGONAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::INVERSE_DIAGONAL():
|
||||
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
|
||||
break;
|
||||
|
||||
case GradientType::RADIAL():
|
||||
$this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
|
||||
$this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
|
||||
break;
|
||||
}
|
||||
|
||||
$toBeHashed = $this->getColorString($startColor) . $this->getColorString($endColor) . $gradient->getType();
|
||||
if ($startColor instanceof Alpha) {
|
||||
$toBeHashed .= (string) $startColor->getAlpha();
|
||||
}
|
||||
$id = sprintf('g%d-%s', ++$this->gradientCount, hash('xxh3', $toBeHashed));
|
||||
$this->xmlWriter->writeAttribute('id', $id);
|
||||
|
||||
$this->xmlWriter->startElement('stop');
|
||||
$this->xmlWriter->writeAttribute('offset', '0%');
|
||||
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
|
||||
|
||||
if ($startColor instanceof Alpha) {
|
||||
$this->xmlWriter->writeAttribute('stop-opacity', (string) $startColor->getAlpha());
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
|
||||
$this->xmlWriter->startElement('stop');
|
||||
$this->xmlWriter->writeAttribute('offset', '100%');
|
||||
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
|
||||
|
||||
if ($endColor instanceof Alpha) {
|
||||
$this->xmlWriter->writeAttribute('stop-opacity', (string) $endColor->getAlpha());
|
||||
}
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
|
||||
$this->xmlWriter->endElement();
|
||||
$this->xmlWriter->endElement();
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function getColorString(ColorInterface $color) : string
|
||||
{
|
||||
$color = $color->toRgb();
|
||||
|
||||
return sprintf(
|
||||
'#%02x%02x%02x',
|
||||
$color->getRed(),
|
||||
$color->getGreen(),
|
||||
$color->getBlue()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Image;
|
||||
|
||||
final class TransformationMatrix
|
||||
{
|
||||
/**
|
||||
* @var float[]
|
||||
*/
|
||||
private array $values;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->values = [1, 0, 0, 1, 0, 0];
|
||||
}
|
||||
|
||||
public function multiply(self $other) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1];
|
||||
$matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1];
|
||||
$matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3];
|
||||
$matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3];
|
||||
$matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5]
|
||||
+ $this->values[4];
|
||||
$matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5]
|
||||
+ $this->values[5];
|
||||
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
public static function scale(float $size) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$matrix->values = [$size, 0, 0, $size, 0, 0];
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
public static function translate(float $x, float $y) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$matrix->values = [1, 0, 0, 1, $x, $y];
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
public static function rotate(int $degrees) : self
|
||||
{
|
||||
$matrix = new self();
|
||||
$rad = deg2rad($degrees);
|
||||
$matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0];
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies this matrix onto a point and returns the resulting viewport point.
|
||||
*
|
||||
* @return float[]
|
||||
*/
|
||||
public function apply(float $x, float $y) : array
|
||||
{
|
||||
return [
|
||||
$x * $this->values[0] + $y * $this->values[2] + $this->values[4],
|
||||
$x * $this->values[1] + $y * $this->values[3] + $this->values[5],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer;
|
||||
|
||||
use BaconQrCode\Encoder\MatrixUtil;
|
||||
use BaconQrCode\Encoder\QrCode;
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use BaconQrCode\Renderer\Image\ImageBackEndInterface;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
use BaconQrCode\Renderer\RendererStyle\EyeFill;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
|
||||
final class ImageRenderer implements RendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RendererStyle $rendererStyle,
|
||||
private readonly ImageBackEndInterface $imageBackEnd
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if matrix width doesn't match height
|
||||
*/
|
||||
public function render(QrCode $qrCode) : string
|
||||
{
|
||||
$size = $this->rendererStyle->getSize();
|
||||
$margin = $this->rendererStyle->getMargin();
|
||||
$matrix = $qrCode->getMatrix();
|
||||
$matrixSize = $matrix->getWidth();
|
||||
|
||||
if ($matrixSize !== $matrix->getHeight()) {
|
||||
throw new InvalidArgumentException('Matrix must have the same width and height');
|
||||
}
|
||||
|
||||
$totalSize = $matrixSize + ($margin * 2);
|
||||
$moduleSize = $size / $totalSize;
|
||||
$fill = $this->rendererStyle->getFill();
|
||||
|
||||
$this->imageBackEnd->new($size, $fill->getBackgroundColor());
|
||||
$this->imageBackEnd->scale((float) $moduleSize);
|
||||
$this->imageBackEnd->translate((float) $margin, (float) $margin);
|
||||
|
||||
$module = $this->rendererStyle->getModule();
|
||||
$moduleMatrix = clone $matrix;
|
||||
MatrixUtil::removePositionDetectionPatterns($moduleMatrix);
|
||||
$modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix));
|
||||
|
||||
if ($fill->hasGradientFill()) {
|
||||
$this->imageBackEnd->drawPathWithGradient(
|
||||
$modulePath,
|
||||
$fill->getForegroundGradient(),
|
||||
0,
|
||||
0,
|
||||
$matrixSize,
|
||||
$matrixSize
|
||||
);
|
||||
} else {
|
||||
$this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor());
|
||||
}
|
||||
|
||||
return $this->imageBackEnd->done();
|
||||
}
|
||||
|
||||
private function drawEyes(int $matrixSize, Path $modulePath) : Path
|
||||
{
|
||||
$fill = $this->rendererStyle->getFill();
|
||||
|
||||
$eye = $this->rendererStyle->getEye();
|
||||
$externalPath = $eye->getExternalPath();
|
||||
$internalPath = $eye->getInternalPath();
|
||||
|
||||
$modulePath = $this->drawEye(
|
||||
$externalPath,
|
||||
$internalPath,
|
||||
$fill->getTopLeftEyeFill(),
|
||||
3.5,
|
||||
3.5,
|
||||
0,
|
||||
$modulePath
|
||||
);
|
||||
$modulePath = $this->drawEye(
|
||||
$externalPath,
|
||||
$internalPath,
|
||||
$fill->getTopRightEyeFill(),
|
||||
$matrixSize - 3.5,
|
||||
3.5,
|
||||
90,
|
||||
$modulePath
|
||||
);
|
||||
$modulePath = $this->drawEye(
|
||||
$externalPath,
|
||||
$internalPath,
|
||||
$fill->getBottomLeftEyeFill(),
|
||||
3.5,
|
||||
$matrixSize - 3.5,
|
||||
-90,
|
||||
$modulePath
|
||||
);
|
||||
|
||||
return $modulePath;
|
||||
}
|
||||
|
||||
private function drawEye(
|
||||
Path $externalPath,
|
||||
Path $internalPath,
|
||||
EyeFill $fill,
|
||||
float $xTranslation,
|
||||
float $yTranslation,
|
||||
int $rotation,
|
||||
Path $modulePath
|
||||
) : Path {
|
||||
if ($fill->inheritsBothColors()) {
|
||||
return $modulePath
|
||||
->append(
|
||||
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
|
||||
)
|
||||
->append(
|
||||
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
|
||||
);
|
||||
}
|
||||
|
||||
$this->imageBackEnd->push();
|
||||
$this->imageBackEnd->translate($xTranslation, $yTranslation);
|
||||
|
||||
if (0 !== $rotation) {
|
||||
$this->imageBackEnd->rotate($rotation);
|
||||
}
|
||||
|
||||
if ($fill->inheritsExternalColor()) {
|
||||
$modulePath = $modulePath->append(
|
||||
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
|
||||
);
|
||||
} else {
|
||||
$this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
|
||||
}
|
||||
|
||||
if ($fill->inheritsInternalColor()) {
|
||||
$modulePath = $modulePath->append(
|
||||
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
|
||||
);
|
||||
} else {
|
||||
$this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
|
||||
}
|
||||
|
||||
$this->imageBackEnd->pop();
|
||||
|
||||
return $modulePath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Module;
|
||||
|
||||
use BaconQrCode\Encoder\ByteMatrix;
|
||||
use BaconQrCode\Exception\InvalidArgumentException;
|
||||
use BaconQrCode\Renderer\Path\Path;
|
||||
|
||||
/**
|
||||
* Renders individual modules as dots.
|
||||
*/
|
||||
final class DotsModule implements ModuleInterface
|
||||
{
|
||||
public const LARGE = 1;
|
||||
public const MEDIUM = .8;
|
||||
public const SMALL = .6;
|
||||
|
||||
public function __construct(private readonly float $size)
|
||||
{
|
||||
if ($size <= 0 || $size > 1) {
|
||||
throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)');
|
||||
}
|
||||
}
|
||||
|
||||
public function createPath(ByteMatrix $matrix) : Path
|
||||
{
|
||||
$width = $matrix->getWidth();
|
||||
$height = $matrix->getHeight();
|
||||
$path = new Path();
|
||||
$halfSize = $this->size / 2;
|
||||
$margin = (1 - $this->size) / 2;
|
||||
|
||||
for ($y = 0; $y < $height; ++$y) {
|
||||
for ($x = 0; $x < $width; ++$x) {
|
||||
if (! $matrix->get($x, $y)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pathX = $x + $margin;
|
||||
$pathY = $y + $margin;
|
||||
|
||||
$path = $path
|
||||
->move($pathX + $this->size, $pathY + $halfSize)
|
||||
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY + $this->size)
|
||||
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX, $pathY + $halfSize)
|
||||
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY)
|
||||
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $this->size, $pathY + $halfSize)
|
||||
->close()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Module\EdgeIterator;
|
||||
|
||||
final class Edge
|
||||
{
|
||||
/**
|
||||
* @var array<int[]>
|
||||
*/
|
||||
private array $points = [];
|
||||
|
||||
/**
|
||||
* @var array<int[]>|null
|
||||
*/
|
||||
private ?array $simplifiedPoints = null;
|
||||
|
||||
private int $minX = PHP_INT_MAX;
|
||||
|
||||
private int $minY = PHP_INT_MAX;
|
||||
|
||||
private int $maxX = -1;
|
||||
|
||||
private int $maxY = -1;
|
||||
|
||||
public function __construct(private readonly bool $positive)
|
||||
{
|
||||
}
|
||||
|
||||
public function addPoint(int $x, int $y) : void
|
||||
{
|
||||
$this->points[] = [$x, $y];
|
||||
$this->minX = min($this->minX, $x);
|
||||
$this->minY = min($this->minY, $y);
|
||||
$this->maxX = max($this->maxX, $x);
|
||||
$this->maxY = max($this->maxY, $y);
|
||||
}
|
||||
|
||||
public function isPositive() : bool
|
||||
{
|
||||
return $this->positive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int[]>
|
||||
*/
|
||||
public function getPoints() : array
|
||||
{
|
||||
return $this->points;
|
||||
}
|
||||
|
||||
public function getMaxX() : int
|
||||
{
|
||||
return $this->maxX;
|
||||
}
|
||||
|
||||
public function getSimplifiedPoints() : array
|
||||
{
|
||||
if (null !== $this->simplifiedPoints) {
|
||||
return $this->simplifiedPoints;
|
||||
}
|
||||
|
||||
$points = [];
|
||||
$length = count($this->points);
|
||||
|
||||
for ($i = 0; $i < $length; ++$i) {
|
||||
$previousPoint = $this->points[(0 === $i ? $length : $i) - 1];
|
||||
$nextPoint = $this->points[($length - 1 === $i ? -1 : $i) + 1];
|
||||
$currentPoint = $this->points[$i];
|
||||
|
||||
if (($previousPoint[0] === $currentPoint[0] && $currentPoint[0] === $nextPoint[0])
|
||||
|| ($previousPoint[1] === $currentPoint[1] && $currentPoint[1] === $nextPoint[1])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$points[] = $currentPoint;
|
||||
}
|
||||
|
||||
return $this->simplifiedPoints = $points;
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace BaconQrCode\Renderer\Module\EdgeIterator;
|
||||
|
||||
use BaconQrCode\Encoder\ByteMatrix;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Edge iterator based on potrace.
|
||||
*/
|
||||
final class EdgeIterator implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private array $bytes = [];
|
||||
|
||||
private ?int $size;
|
||||
|
||||
private int $width;
|
||||
|
||||
private int $height;
|
||||
|
||||
public function __construct(ByteMatrix $matrix)
|
||||
{
|
||||
$this->bytes = iterator_to_array($matrix->getBytes());
|
||||
$this->size = count($this->bytes);
|
||||
$this->width = $matrix->getWidth();
|
||||
$this->height = $matrix->getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable<Edge>
|
||||
*/
|
||||
public function getIterator() : Traversable
|
||||
{
|
||||
$originalBytes = $this->bytes;
|
||||
$point = $this->findNext(0, 0);
|
||||
|
||||
while (null !== $point) {
|
||||
$edge = $this->findEdge($point[0], $point[1]);
|
||||
$this->xorEdge($edge);
|
||||
|
||||
yield $edge;
|
||||
|
||||
$point = $this->findNext($point[0], $point[1]);
|
||||
}
|
||||
|
||||
$this->bytes = $originalBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]|null
|
||||
*/
|
||||
private function findNext(int $x, int $y) : ?array
|
||||
{
|
||||
$i = $this->width * $y + $x;
|
||||
|
||||
while ($i < $this->size && 1 !== $this->bytes[$i]) {
|
||||
++$i;
|
||||
}
|
||||
|
||||
if ($i < $this->size) {
|
||||
return $this->pointOf($i);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function findEdge(int $x, int $y) : Edge
|
||||
{
|
||||
$edge = new Edge($this->isSet($x, $y));
|
||||
$startX = $x;
|
||||
$startY = $y;
|
||||
$dirX = 0;
|
||||
$dirY = 1;
|
||||
|
||||
while (true) {
|
||||
$edge->addPoint($x, $y);
|
||||
$x += $dirX;
|
||||
$y += $dirY;
|
||||
|
||||
if ($x === $startX && $y === $startY) {
|
||||
break;
|
||||
}
|
||||
|
||||
$left = $this->isSet($x + ($dirX + $dirY - 1 ) / 2, $y + ($dirY - $dirX - 1) / 2);
|
||||
$right = $this->isSet($x + ($dirX - $dirY - 1) / 2, $y + ($dirY + $dirX - 1) / 2);
|
||||
|
||||
if ($right && ! $left) {
|
||||
$tmp = $dirX;
|
||||
$dirX = -$dirY;
|
||||
$dirY = $tmp;
|
||||
} elseif ($right) {
|
||||
$tmp = $dirX;
|
||||
$dirX = -$dirY;
|
||||
$dirY = $tmp;
|
||||
} elseif (! $left) {
|
||||
$tmp = $dirX;
|
||||
$dirX = $dirY;
|
||||
$dirY = -$tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return $edge;
|
||||
}
|
||||
|
||||
private function xorEdge(Edge $path) : void
|
||||
{
|
||||
$points = $path->getPoints();
|
||||
$y1 = $points[0][1];
|
||||
$length = count($points);
|
||||
$maxX = $path->getMaxX();
|
||||
|
||||
for ($i = 1; $i < $length; ++$i) {
|
||||
$y = $points[$i][1];
|
||||
|
||||
if ($y === $y1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$x = $points[$i][0];
|
||||
$minY = min($y1, $y);
|
||||
|
||||
for ($j = $x; $j < $maxX; ++$j) {
|
||||
$this->flip($j, $minY);
|
||||
}
|
||||
|
||||
$y1 = $y;
|
||||
}
|
||||
}
|
||||
|
||||
private function isSet(int $x, int $y) : bool
|
||||
{
|
||||
return (
|
||||
$x >= 0
|
||||
&& $x < $this->width
|
||||
&& $y >= 0
|
||||
&& $y < $this->height
|
||||
) && 1 === $this->bytes[$this->width * $y + $x];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private function pointOf(int $i) : array
|
||||
{
|
||||
$y = intdiv($i, $this->width);
|
||||
return [$i - $y * $this->width, $y];
|
||||
}
|
||||
|
||||
private function flip(int $x, int $y) : void
|
||||
{
|
||||
$this->bytes[$this->width * $y + $x] = (
|
||||
$this->isSet($x, $y) ? 0 : 1
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user