Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions examples/contribuinte/emitir_com_certificado_em_memoria.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

use Nfse\Dto\Nfse\DpsData;
use Nfse\Enums\TipoAmbiente;
use Nfse\Http\NfseContext;
use Nfse\Nfse;
use Nfse\Support\IdGenerator;

require_once __DIR__.'/../../vendor/autoload.php';

// 1. Lendo de banco de dados
// $pfxContent = $pdo->query("SELECT conteudo FROM certificados WHERE id = 1")->fetchColumn();

// 2. Lendo de variável de ambiente em base64
// $pfxContent = base64_decode(getenv('CERTIFICADO_PFX_BASE64'));

// 3. Lendo de um arquivo (equivalente ao comportamento anterior)
$pfxContent = file_get_contents(__DIR__.'/../certs/contribuinte.pfx');

$certificatePassword = '[PASSWORD]';

// Passa certificateContent em vez de certificatePath
$context = new NfseContext(
ambiente: TipoAmbiente::Homologacao,
certificatePath: null,
certificatePassword: $certificatePassword,
certificateContent: $pfxContent,
);

$nfse = new Nfse($context);

try {
$cnpjPrestador = '03279735000194';
$codigoMunicipio = '2304400';
$serie = '1';
$numero = '100';

$idDps = IdGenerator::generateDpsId(
cpfCnpj: $cnpjPrestador,
codIbge: $codigoMunicipio,
serieDps: $serie,
numDps: $numero
);

$dps = new DpsData([
'@attributes' => ['versao' => '1.00'],
'infDPS' => [
'@attributes' => ['Id' => $idDps],
'tpAmb' => 2,
'dhEmi' => date('c'),
'verAplic' => 'SDK-PHP-1.0',
'serie' => $serie,
'nDPS' => $numero,
'dCompet' => date('Y-m-d'),
'tpEmit' => 1,
'cLocEmi' => $codigoMunicipio,
'prest' => [
'CNPJ' => $cnpjPrestador,
'xNome' => 'Empresa de Teste',
'end' => [
'endNac' => ['cMun' => $codigoMunicipio, 'CEP' => '60000000'],
'xLgr' => 'Rua Teste',
'nro' => '123',
'xBairro' => 'Centro',
],
'regTrib' => ['opSimpNac' => 1, 'regEspTrib' => 0],
],
'toma' => [
'CNPJ' => '44827692000111',
'xNome' => 'Cliente de Teste',
],
'serv' => [
'locPrest' => ['cLocPrestacao' => $codigoMunicipio],
'cServ' => ['cTribNac' => '010101', 'xDescServ' => 'Desenvolvimento de Software'],
],
'valores' => [
'vServPrest' => ['vServ' => 100.00],
'trib' => [
'tribMun' => ['tribISSQN' => 1, 'tpRetISSQN' => 1],
'tribFed' => ['piscofins' => ['CST' => '08']],
'totTrib' => ['indTotTrib' => 0],
],
],
],
]);

$nfseData = $nfse->contribuinte()->emitir($dps);

echo "NFS-e emitida com sucesso!\n";
echo 'Chave de Acesso: '.$nfseData->infNfse->id."\n";

} catch (\Exception $e) {
echo 'Erro: '.$e->getMessage()."\n";
}
23 changes: 22 additions & 1 deletion src/Http/Client/AdnClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,32 @@ class AdnClient implements AdnDanfseInterface

private Client $httpClient;

private ?string $tempCertFile = null;

public function __construct(private NfseContext $context)
{
$this->httpClient = $this->createHttpClient();
}

public function __destruct()
{
if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) {
unlink($this->tempCertFile);
}
}

private function resolveCertificatePath(): string
{
if ($this->context->certificatePath !== null) {
return $this->context->certificatePath;
}

$this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_');
file_put_contents($this->tempCertFile, $this->context->certificateContent);

return $this->tempCertFile;
}

private function createHttpClient(): Client
{
$baseUrl = $this->context->ambiente === TipoAmbiente::Producao
Expand All @@ -41,7 +62,7 @@ private function createHttpClient(): Client
'base_uri' => $baseUrl,
'curl' => [
CURLOPT_SSLCERTTYPE => 'P12',
CURLOPT_SSLCERT => $this->context->certificatePath,
CURLOPT_SSLCERT => $this->resolveCertificatePath(),
CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
CURLOPT_CONNECTTIMEOUT => 30,
Expand Down
23 changes: 22 additions & 1 deletion src/Http/Client/CncClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,32 @@ class CncClient

private Client $httpClient;

private ?string $tempCertFile = null;

public function __construct(private NfseContext $context)
{
$this->httpClient = $this->createHttpClient();
}

public function __destruct()
{
if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) {
unlink($this->tempCertFile);
}
}

private function resolveCertificatePath(): string
{
if ($this->context->certificatePath !== null) {
return $this->context->certificatePath;
}

$this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_');
file_put_contents($this->tempCertFile, $this->context->certificateContent);

return $this->tempCertFile;
}

private function createHttpClient(): Client
{
$baseUrl = $this->context->ambiente === TipoAmbiente::Producao
Expand All @@ -33,7 +54,7 @@ private function createHttpClient(): Client
'base_uri' => $baseUrl,
'curl' => [
CURLOPT_SSLCERTTYPE => 'P12',
CURLOPT_SSLCERT => $this->context->certificatePath,
CURLOPT_SSLCERT => $this->resolveCertificatePath(),
CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
CURLOPT_CONNECTTIMEOUT => 30,
Expand Down
23 changes: 22 additions & 1 deletion src/Http/Client/SefinClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,41 @@ class SefinClient implements SefinNacionalInterface

private string $baseUrl;

private ?string $tempCertFile = null;

public function __construct(private NfseContext $context)
{
$resolver = new SefinEndpointResolver();
$this->baseUrl = $resolver->resolve($this->context);
$this->httpClient = $this->createHttpClient();
}

public function __destruct()
{
if ($this->tempCertFile !== null && file_exists($this->tempCertFile)) {
unlink($this->tempCertFile);
}
}

private function resolveCertificatePath(): string
{
if ($this->context->certificatePath !== null) {
return $this->context->certificatePath;
}

$this->tempCertFile = tempnam(sys_get_temp_dir(), 'nfse_cert_');
file_put_contents($this->tempCertFile, $this->context->certificateContent);

return $this->tempCertFile;
}

private function createHttpClient(): Client
{
return new Client([
'base_uri' => rtrim($this->baseUrl, '/') . '/',
'curl' => [
CURLOPT_SSLCERTTYPE => 'P12',
CURLOPT_SSLCERT => $this->context->certificatePath,
CURLOPT_SSLCERT => $this->resolveCertificatePath(),
CURLOPT_SSLCERTPASSWD => $this->context->certificatePassword,
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
CURLOPT_CONNECTTIMEOUT => 30,
Expand Down
9 changes: 7 additions & 2 deletions src/Http/NfseContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ final class NfseContext
{
public function __construct(
public TipoAmbiente $ambiente,
public string $certificatePath,
public ?string $certificatePath,
public string $certificatePassword,
public ?string $codigoMunicipio = null,
public ?\Nfse\Dto\Http\Endpoint $endpoint = null,
) {}
public ?string $certificateContent = null,
) {
if ($certificatePath === null && $certificateContent === null) {
throw new \InvalidArgumentException('Informe certificatePath ou certificateContent.');
}
}
}
13 changes: 11 additions & 2 deletions src/Service/ContribuinteService.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function emitir(DpsData $dps): NfseData
$builder = new DpsXmlBuilder;
$xml = $builder->build($dps);

$cert = new Certificate($this->context->certificatePath, $this->context->certificatePassword);
$cert = $this->makeCertificate();
$signer = $this->createSigner($cert);

// Assina a tag 'infDPS'
Expand Down Expand Up @@ -112,7 +112,7 @@ public function registrarEventoData(PedRegEventoData $evento): \Nfse\Dto\Http\Re
$builder = new EventosXmlBuilder;
$xml = $builder->buildPedRegEvento($evento);

$cert = new Certificate($this->context->certificatePath, $this->context->certificatePassword);
$cert = $this->makeCertificate();
$signer = $this->createSigner($cert);

// Assina a tag 'infPedReg'
Expand Down Expand Up @@ -199,4 +199,13 @@ protected function createSigner(Certificate $certificate): SignerInterface
{
return new XmlSigner($certificate);
}

private function makeCertificate(): Certificate
{
if ($this->context->certificateContent !== null) {
return Certificate::fromContent($this->context->certificateContent, $this->context->certificatePassword);
}

return new Certificate($this->context->certificatePath, $this->context->certificatePassword);
}
}
10 changes: 10 additions & 0 deletions src/Signer/Certificate.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ public function __construct(string $pfxPath, string $password)
$this->load();
}

public static function fromContent(string $pfxContent, string $password): self
{
$instance = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
$instance->pfxContent = $pfxContent;
$instance->password = $password;
$instance->load();

return $instance;
}

private function load(): void
{
if (! openssl_pkcs12_read($this->pfxContent, $certs, $this->password)) {
Expand Down
34 changes: 34 additions & 0 deletions tests/Unit/Signer/CertificateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,37 @@
->and($cert->getCleanCertificate())->not->toContain('BEGIN CERTIFICATE')
->and($cert->sign('content'))->toBeString();
});

it('fromContent loads certificate from raw bytes', function () {
$pfxContent = file_get_contents(__DIR__.'/../../fixtures/certs/test.pfx');

$cert = Certificate::fromContent($pfxContent, '1234');

expect($cert->getPrivateKey())->toBeString()
->and($cert->getCertificate())->toBeString()
->and($cert->getCleanCertificate())->not->toContain('BEGIN CERTIFICATE')
->and($cert->sign('content'))->toBeString();
});

it('fromContent produces the same output as fromPath', function () {
$pfxPath = __DIR__.'/../../fixtures/certs/test.pfx';
$pfxContent = file_get_contents($pfxPath);

$certFromPath = new Certificate($pfxPath, '1234');
$certFromContent = Certificate::fromContent($pfxContent, '1234');

expect($certFromContent->getCertificate())->toBe($certFromPath->getCertificate())
->and($certFromContent->getCleanCertificate())->toBe($certFromPath->getCleanCertificate());
});

it('fromContent throws on wrong password', function () {
$pfxContent = file_get_contents(__DIR__.'/../../fixtures/certs/test.pfx');

expect(fn () => Certificate::fromContent($pfxContent, 'wrong_password'))
->toThrow(Exception::class, 'Senha do certificado incorreta ou arquivo inválido/corrompido');
});

it('fromContent throws on invalid pfx bytes', function () {
expect(fn () => Certificate::fromContent('not-a-pfx-file', 'any'))
->toThrow(Exception::class, 'Senha do certificado incorreta ou arquivo inválido/corrompido');
});
Loading