Skip to content

Commit 56ce53a

Browse files
committed
feat: add PHP code skeleton
1 parent f788c2b commit 56ce53a

File tree

5 files changed

+368
-0
lines changed

5 files changed

+368
-0
lines changed

src/Exception/ParserException.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bfabio\PublicCodeParser\Exception;
6+
7+
class ParserException extends \RuntimeException
8+
{
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bfabio\PublicCodeParser\Exception;
6+
7+
class ValidationException extends ParserException
8+
{
9+
private array $errors;
10+
11+
public function __construct(string $message, array $errors = [], ?\Throwable $previous = null)
12+
{
13+
parent::__construct($message, 0, $previous);
14+
$this->errors = $errors;
15+
}
16+
17+
public function getErrors(): array
18+
{
19+
return $this->errors;
20+
}
21+
}

src/Parser.php

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bfabio\PublicCodeParser;
6+
7+
use FFI;
8+
use Bfabio\PublicCodeParser\Exception\ParserException;
9+
use Bfabio\PublicCodeParser\Exception\ValidationException;
10+
11+
class Parser
12+
{
13+
private ParserOptions $options;
14+
private FFI $ffi;
15+
private static ?FFI $ffiInstance = null;
16+
17+
public function __construct(?ParserOptions $options = null)
18+
{
19+
$this->options = $options ?? new ParserOptions();
20+
$this->ffi = $this->getFFI();
21+
}
22+
23+
/**
24+
* Parse publiccode.yml content
25+
*
26+
* @param string $content YAML content
27+
* @return PublicCode
28+
* @throws ParserException
29+
* @throws ValidationException
30+
*/
31+
public function parse(string $content): PublicCode
32+
{
33+
$result = $this->ffi->ParseString($content);
34+
35+
if ($result === null) {
36+
throw new ParserException('Failed to parse publiccode.yml content');
37+
}
38+
39+
return $this->processResult($result);
40+
}
41+
42+
/**
43+
* Parse publiccode.yml file
44+
*
45+
* @param string $filePath Path to publiccode.yml
46+
* @return PublicCode
47+
* @throws ParserException
48+
* @throws ValidationException
49+
*/
50+
public function parseFile(string $filePath): PublicCode
51+
{
52+
if (!file_exists($filePath)) {
53+
throw new ParserException("File not found: {$filePath}");
54+
}
55+
56+
$options = FFI::new('struct ParseOptions');
57+
$options->DisableNetwork = $this->options->isDisableNetwork();
58+
59+
$result = $this->ffi->ParseFile($filePath, FFI::addr($options));
60+
61+
if ($result === null) {
62+
throw new ParserException('Failed to parse publiccode.yml file');
63+
}
64+
65+
return $this->processResult($result);
66+
}
67+
68+
/**
69+
* Validate publiccode.yml file without parsing
70+
*
71+
* @param string $filePath
72+
* @return bool
73+
*/
74+
public function validate(string $filePath): bool
75+
{
76+
try {
77+
$this->parseFile($filePath);
78+
return true;
79+
} catch (ParserException | ValidationException $e) {
80+
return false;
81+
}
82+
}
83+
84+
/**
85+
* Get or create FFI instance
86+
*
87+
* @return FFI
88+
* @throws ParserException
89+
*/
90+
private function getFFI(): FFI
91+
{
92+
if (self::$ffiInstance !== null) {
93+
return self::$ffiInstance;
94+
}
95+
96+
$libraryPath = $this->findLibrary();
97+
98+
$cdef = <<<'CDEF'
99+
typedef struct {
100+
bool DisableNetwork;
101+
char *Branch;
102+
char *BaseURL;
103+
} ParseOptions;
104+
105+
typedef struct {
106+
char* Data;
107+
char* Error;
108+
int ErrorCount;
109+
char** Errors;
110+
} ParseResult;
111+
112+
ParseResult* ParseFile(const char* filepath, ParseOptions* options);
113+
ParseResult* ParseString(const char* content);
114+
void FreeResult(ParseResult* result);
115+
CDEF;
116+
117+
try {
118+
self::$ffiInstance = FFI::cdef($cdef, $libraryPath);
119+
return self::$ffiInstance;
120+
} catch (\FFI\Exception $e) {
121+
throw new ParserException(
122+
'Failed to load publiccode-parser library: ' . $e->getMessage(),
123+
0,
124+
$e
125+
);
126+
}
127+
}
128+
129+
/**
130+
* Find the publiccode-parser shared library
131+
*
132+
* @return string
133+
* @throws ParserException
134+
*/
135+
private function findLibrary(): string
136+
{
137+
$possiblePaths = [
138+
__DIR__ . '/',
139+
__DIR__ . '/../lib/',
140+
__DIR__ . '/../vendor/lib/',
141+
'/usr/local/lib/',
142+
'/usr/lib/'
143+
];
144+
145+
foreach ($possiblePaths as $path) {
146+
if (file_exists($path . 'libpubliccode-parser.so')) {
147+
return $path;
148+
}
149+
}
150+
151+
throw new ParserException('libpubliccode-parser.so not found');
152+
}
153+
154+
/**
155+
* Process FFI result and convert to PublicCode object
156+
*
157+
* @param mixed $result FFI ParseResult pointer
158+
* @return PublicCode
159+
* @throws ValidationException
160+
* @throws ParserException
161+
*/
162+
private function processResult($result): PublicCode
163+
{
164+
if ($result->Error !== null) {
165+
$errors = [];
166+
167+
if ($result->ErrorCount > 0 && $result->Errors !== null) {
168+
for ($i = 0; $i < $result->ErrorCount; $i++) {
169+
$errors[] = FFI::string($result->Errors[$i]);
170+
}
171+
}
172+
173+
$errorMessage = FFI::string($result->Error);
174+
$this->ffi->FreeResult($result);
175+
176+
throw new ValidationException($errorMessage, $errors);
177+
}
178+
179+
if ($result->Data === null) {
180+
$this->ffi->FreeResult($result);
181+
throw new ParserException('No data returned from parser');
182+
}
183+
184+
$jsonData = FFI::string($result->Data);
185+
$this->ffi->FreeResult($result);
186+
187+
$data = json_decode($jsonData, true);
188+
189+
if (json_last_error() !== JSON_ERROR_NONE) {
190+
throw new ParserException('Failed to decode JSON data: ' . json_last_error_msg());
191+
}
192+
193+
return new PublicCode($data);
194+
}
195+
}

src/ParserOptions.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bfabio\PublicCodeParser;
6+
7+
class ParserOptions
8+
{
9+
private bool $disableNetwork = true;
10+
private int $timeout = 60;
11+
12+
public function isNetworkDisabled(): bool
13+
{
14+
return $this->disableNetwork;
15+
}
16+
17+
public function setDisableNetwork(bool $disableNetwork): self
18+
{
19+
$this->disableNetwork = $disableNetwork;
20+
return $this;
21+
}
22+
}

src/PublicCode.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bfabio\PublicCodeParser;
6+
7+
use JsonSerializable;
8+
9+
class PublicCode implements JsonSerializable
10+
{
11+
private array $data;
12+
13+
public function __construct(array $data)
14+
{
15+
$this->data = $data;
16+
}
17+
18+
public function getName(): string
19+
{
20+
return $this->data['name'];
21+
}
22+
23+
public function getUrl(): string
24+
{
25+
return $this->data['url'];
26+
}
27+
28+
public function getLandingUrl(): ?string
29+
{
30+
return $this->data['landingURL'] ?? null;
31+
}
32+
33+
public function getDescription(string $language = 'en'): ?string
34+
{
35+
return $this->data['description'][$language]['shortDescription'] ?? null;
36+
}
37+
38+
public function getLongDescription(string $language = 'en'): ?string
39+
{
40+
return $this->data['description'][$language]['longDescription'] ?? null;
41+
}
42+
43+
public function getAllDescriptions(): array
44+
{
45+
return $this->data['description'];
46+
}
47+
48+
public function getFeatures(string $language = 'en'): array
49+
{
50+
return $this->data['description'][$language]['features'] ?? [];
51+
}
52+
53+
public function getMaintenance(): array
54+
{
55+
return $this->data['maintenance'];
56+
}
57+
58+
public function getLicense(): ?string
59+
{
60+
return $this->data['legal']['license'];
61+
}
62+
63+
public function getRepoOwner(): ?string
64+
{
65+
return $this->data['legal']['repoOwner'] ?? null;
66+
}
67+
68+
public function getCategories(): array
69+
{
70+
return $this->data['categories'];
71+
}
72+
73+
public function getPlatforms(): array
74+
{
75+
return $this->data['platforms'];
76+
}
77+
78+
public function getReleaseDate(): ?\DateTimeInterface
79+
{
80+
if (!isset($this->data['releaseDate'])) {
81+
return null;
82+
}
83+
84+
return new \DateTime($this->data['releaseDate']);
85+
}
86+
87+
public function getDevelopmentStatus(): string
88+
{
89+
return $this->data['developmentStatus'];
90+
}
91+
92+
public function getSoftwareType(): string
93+
{
94+
return $this->data['softwareType'];
95+
}
96+
97+
public function get(string $key, $default = null)
98+
{
99+
return $this->data[$key] ?? $default;
100+
}
101+
102+
public function has(string $key): bool
103+
{
104+
return isset($this->data[$key]);
105+
}
106+
107+
public function toArray(): array
108+
{
109+
return $this->data;
110+
}
111+
112+
public function toJson(int $options = 0): string
113+
{
114+
return json_encode($this->data, $options);
115+
}
116+
117+
public function jsonSerialize(): array
118+
{
119+
return $this->data;
120+
}
121+
}

0 commit comments

Comments
 (0)