Skip to content

Commit cc8cd20

Browse files
feat(extgen): add support for //export_php:namespace
1 parent 6be2611 commit cc8cd20

15 files changed

+857
-7
lines changed

docs/extensions.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,61 @@ func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsaf
335335
}
336336
```
337337

338+
### Using Namespaces
339+
340+
The generator supports organizing your PHP extension's functions, classes, and constants under a namespace using the `//export_php:namespace` directive. This helps avoid naming conflicts and provides better organization for your extension's API.
341+
342+
#### Declaring a Namespace
343+
344+
Use the `//export_php:namespace` directive at the top of your Go file to place all exported symbols under a specific namespace:
345+
346+
```go
347+
//export_php:namespace My\Extension
348+
package main
349+
350+
import "C"
351+
352+
//export_php:function hello(): string
353+
func hello() string {
354+
return "Hello from My\\Extension namespace!"
355+
}
356+
357+
//export_php:class User
358+
type UserStruct struct {
359+
// internal fields
360+
}
361+
362+
//export_php:method User::getName(): string
363+
func (u *UserStruct) GetName() unsafe.Pointer {
364+
return frankenphp.PHPString("John Doe", false)
365+
}
366+
367+
//export_php:const
368+
const STATUS_ACTIVE = 1
369+
```
370+
371+
#### Using Namespaced Extension in PHP
372+
373+
When a namespace is declared, all functions, classes, and constants are placed under that namespace in PHP:
374+
375+
```php
376+
<?php
377+
378+
echo My\Extension\hello(); // "Hello from My\Extension namespace!"
379+
380+
$user = new My\Extension\User();
381+
echo $user->getName(); // "John Doe"
382+
383+
echo My\Extension\STATUS_ACTIVE; // 1
384+
```
385+
386+
#### Important Notes
387+
388+
* Only **one** namespace directive is allowed per file. If multiple namespace directives are found, the generator will return an error.
389+
* The namespace applies to **all** exported symbols in the file: functions, classes, methods, and constants.
390+
* Namespace names follow PHP namespace conventions using backslashes (`\`) as separators.
391+
* If no namespace is declared, symbols are exported to the global namespace as usual.
392+
338393
### Generating the Extension
339394

340395
This is where the magic happens, and your extension can now be generated. You can run the generator with the following command:

docs/fr/extensions.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,61 @@ func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsaf
335335
}
336336
```
337337

338+
### Utilisation des Espaces de Noms
339+
340+
Le générateur prend en charge l'organisation des fonctions, classes et constantes de votre extension PHP sous un espace de noms (namespace) en utilisant la directive `//export_php:namespace`. Cela aide à éviter les conflits de noms et fournit une meilleure organisation pour l'API de votre extension.
341+
342+
#### Déclarer un Espace de Noms
343+
344+
Utilisez la directive `//export_php:namespace` en haut de votre fichier Go pour placer tous les symboles exportés sous un espace de noms spécifique :
345+
346+
```go
347+
//export_php:namespace My\Extension
348+
package main
349+
350+
import "C"
351+
352+
//export_php:function hello(): string
353+
func hello() string {
354+
return "Bonjour depuis l'espace de noms My\\Extension !"
355+
}
356+
357+
//export_php:class User
358+
type UserStruct struct {
359+
// champs internes
360+
}
361+
362+
//export_php:method User::getName(): string
363+
func (u *UserStruct) GetName() unsafe.Pointer {
364+
return frankenphp.PHPString("Jean Dupont", false)
365+
}
366+
367+
//export_php:const
368+
const STATUS_ACTIVE = 1
369+
```
370+
371+
#### Utilisation de l'Extension avec Espace de Noms en PHP
372+
373+
Quand un espace de noms est déclaré, toutes les fonctions, classes et constantes sont placées sous cet espace de noms en PHP :
374+
375+
```php
376+
<?php
377+
378+
echo My\Extension\hello(); // "Bonjour depuis l'espace de noms My\Extension !"
379+
380+
$user = new My\Extension\User();
381+
echo $user->getName(); // "Jean Dupont"
382+
383+
echo My\Extension\STATUS_ACTIVE; // 1
384+
```
385+
386+
#### Notes Importantes
387+
388+
* Seule **une** directive d'espace de noms est autorisée par fichier. Si plusieurs directives d'espace de noms sont trouvées, le générateur retournera une erreur.
389+
* L'espace de noms s'applique à **tous** les symboles exportés dans le fichier : fonctions, classes, méthodes et constantes.
390+
* Les noms d'espaces de noms suivent les conventions des espaces de noms PHP en utilisant les barres obliques inverses (`\`) comme séparateurs.
391+
* Si aucun espace de noms n'est déclaré, les symboles sont exportés vers l'espace de noms global comme d'habitude.
392+
338393
### Générer l'Extension
339394

340395
C'est là que la magie opère, et votre extension peut maintenant être générée. Vous pouvez exécuter le générateur avec la commande suivante :

internal/extgen/cfile.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type cTemplateData struct {
2222
Functions []phpFunction
2323
Classes []phpClass
2424
Constants []phpConstant
25+
Namespace string
2526
}
2627

2728
func (cg *cFileGenerator) generate() error {
@@ -44,22 +45,29 @@ func (cg *cFileGenerator) buildContent() (string, error) {
4445
builder.WriteString(templateContent)
4546

4647
for _, fn := range cg.generator.Functions {
47-
fnGen := PHPFuncGenerator{paramParser: &ParameterParser{}}
48+
fnGen := PHPFuncGenerator{
49+
paramParser: &ParameterParser{},
50+
namespace: cg.generator.Namespace,
51+
}
4852
builder.WriteString(fnGen.generate(fn))
4953
}
5054

5155
return builder.String(), nil
5256
}
5357

5458
func (cg *cFileGenerator) getTemplateContent() (string, error) {
55-
tmpl := template.Must(template.New("cfile").Funcs(sprig.FuncMap()).Parse(cFileContent))
59+
funcMap := sprig.FuncMap()
60+
funcMap["namespacedClassName"] = NamespacedName
61+
62+
tmpl := template.Must(template.New("cfile").Funcs(funcMap).Parse(cFileContent))
5663

5764
var buf bytes.Buffer
5865
if err := tmpl.Execute(&buf, cTemplateData{
5966
BaseName: cg.generator.BaseName,
6067
Functions: cg.generator.Functions,
6168
Classes: cg.generator.Classes,
6269
Constants: cg.generator.Constants,
70+
Namespace: cg.generator.Namespace,
6371
}); err != nil {
6472
return "", err
6573
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package extgen
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"github.com/stretchr/testify/require"
6+
"os"
7+
"testing"
8+
)
9+
10+
func TestNamespacedClassName(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
namespace string
14+
className string
15+
expected string
16+
}{
17+
{
18+
name: "no namespace",
19+
namespace: "",
20+
className: "MySuperClass",
21+
expected: "MySuperClass",
22+
},
23+
{
24+
name: "single level namespace",
25+
namespace: "MyNamespace",
26+
className: "MySuperClass",
27+
expected: "MyNamespace_MySuperClass",
28+
},
29+
{
30+
name: "multi level namespace",
31+
namespace: `Go\Extension`,
32+
className: "MySuperClass",
33+
expected: "Go_Extension_MySuperClass",
34+
},
35+
{
36+
name: "deep namespace",
37+
namespace: `My\Deep\Nested\Namespace`,
38+
className: "TestClass",
39+
expected: "My_Deep_Nested_Namespace_TestClass",
40+
},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
result := NamespacedName(tt.namespace, tt.className)
46+
require.Equal(t, tt.expected, result, "expected %q, got %q", tt.expected, result)
47+
})
48+
}
49+
}
50+
51+
func TestCFileGenerationWithNamespace(t *testing.T) {
52+
content := `package main
53+
54+
//export_php:namespace Go\Extension
55+
56+
//export_php:class MySuperClass
57+
type MySuperClass struct{}
58+
59+
//export_php:method MySuperClass test(): string
60+
func (m *MySuperClass) Test() string {
61+
return "test"
62+
}
63+
`
64+
65+
tmpfile, err := os.CreateTemp("", "test_cfile_namespace_*.go")
66+
require.NoError(t, err, "Failed to create temp file")
67+
defer func() {
68+
err := os.Remove(tmpfile.Name())
69+
assert.NoError(t, err, "Failed to remove temp file: %v", err)
70+
}()
71+
72+
_, err = tmpfile.Write([]byte(content))
73+
require.NoError(t, err, "Failed to write to temp file")
74+
75+
err = tmpfile.Close()
76+
require.NoError(t, err, "Failed to close temp file")
77+
78+
generator := &Generator{
79+
BaseName: "test_extension",
80+
SourceFile: tmpfile.Name(),
81+
BuildDir: t.TempDir(),
82+
Namespace: `Go\Extension`,
83+
Classes: []phpClass{
84+
{
85+
Name: "MySuperClass",
86+
GoStruct: "MySuperClass",
87+
Methods: []phpClassMethod{
88+
{
89+
Name: "test",
90+
PhpName: "test",
91+
Signature: "test(): string",
92+
ReturnType: "string",
93+
ClassName: "MySuperClass",
94+
},
95+
},
96+
},
97+
},
98+
}
99+
100+
cFileGen := cFileGenerator{generator: generator}
101+
contentResult, err := cFileGen.getTemplateContent()
102+
require.NoError(t, err, "error generating C file")
103+
104+
expectedCall := "register_class_Go_Extension_MySuperClass()"
105+
require.Contains(t, contentResult, expectedCall, "C file should contain the standard function call")
106+
107+
oldCall := "register_class_MySuperClass()"
108+
require.NotContains(t, contentResult, oldCall, "C file should not contain old non-namespaced call")
109+
}
110+
111+
func TestCFileGenerationWithoutNamespace(t *testing.T) {
112+
generator := &Generator{
113+
BaseName: "test_extension",
114+
BuildDir: t.TempDir(),
115+
Namespace: "",
116+
Classes: []phpClass{
117+
{
118+
Name: "MySuperClass",
119+
GoStruct: "MySuperClass",
120+
},
121+
},
122+
}
123+
124+
cFileGen := cFileGenerator{generator: generator}
125+
contentResult, err := cFileGen.getTemplateContent()
126+
require.NoError(t, err, "error generating C file")
127+
128+
expectedCall := "register_class_MySuperClass()"
129+
require.Contains(t, contentResult, expectedCall, "C file should not contain the standard function call")
130+
}

0 commit comments

Comments
 (0)