feat(extgen): add support for //export_php:namespace (#1721)

This commit is contained in:
Alexandre Daubois
2025-07-16 12:01:39 +02:00
committed by GitHub
parent 63c742648d
commit 1804e36b93
15 changed files with 857 additions and 7 deletions

View File

@@ -335,6 +335,61 @@ func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsaf
}
```
### Using Namespaces
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.
#### Declaring a Namespace
Use the `//export_php:namespace` directive at the top of your Go file to place all exported symbols under a specific namespace:
```go
//export_php:namespace My\Extension
package main
import "C"
//export_php:function hello(): string
func hello() string {
return "Hello from My\\Extension namespace!"
}
//export_php:class User
type UserStruct struct {
// internal fields
}
//export_php:method User::getName(): string
func (u *UserStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString("John Doe", false)
}
//export_php:const
const STATUS_ACTIVE = 1
```
#### Using Namespaced Extension in PHP
When a namespace is declared, all functions, classes, and constants are placed under that namespace in PHP:
```php
<?php
echo My\Extension\hello(); // "Hello from My\Extension namespace!"
$user = new My\Extension\User();
echo $user->getName(); // "John Doe"
echo My\Extension\STATUS_ACTIVE; // 1
```
#### Important Notes
* Only **one** namespace directive is allowed per file. If multiple namespace directives are found, the generator will return an error.
* The namespace applies to **all** exported symbols in the file: functions, classes, methods, and constants.
* Namespace names follow PHP namespace conventions using backslashes (`\`) as separators.
* If no namespace is declared, symbols are exported to the global namespace as usual.
### Generating the Extension
This is where the magic happens, and your extension can now be generated. You can run the generator with the following command:

View File

@@ -335,6 +335,61 @@ func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsaf
}
```
### Utilisation des Espaces de Noms
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.
#### Déclarer un Espace de Noms
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 :
```go
//export_php:namespace My\Extension
package main
import "C"
//export_php:function hello(): string
func hello() string {
return "Bonjour depuis l'espace de noms My\\Extension !"
}
//export_php:class User
type UserStruct struct {
// champs internes
}
//export_php:method User::getName(): string
func (u *UserStruct) GetName() unsafe.Pointer {
return frankenphp.PHPString("Jean Dupont", false)
}
//export_php:const
const STATUS_ACTIVE = 1
```
#### Utilisation de l'Extension avec Espace de Noms en PHP
Quand un espace de noms est déclaré, toutes les fonctions, classes et constantes sont placées sous cet espace de noms en PHP :
```php
<?php
echo My\Extension\hello(); // "Bonjour depuis l'espace de noms My\Extension !"
$user = new My\Extension\User();
echo $user->getName(); // "Jean Dupont"
echo My\Extension\STATUS_ACTIVE; // 1
```
#### Notes Importantes
* 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.
* L'espace de noms s'applique à **tous** les symboles exportés dans le fichier : fonctions, classes, méthodes et constantes.
* Les noms d'espaces de noms suivent les conventions des espaces de noms PHP en utilisant les barres obliques inverses (`\`) comme séparateurs.
* Si aucun espace de noms n'est déclaré, les symboles sont exportés vers l'espace de noms global comme d'habitude.
### Générer l'Extension
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 :

View File

@@ -22,6 +22,7 @@ type cTemplateData struct {
Functions []phpFunction
Classes []phpClass
Constants []phpConstant
Namespace string
}
func (cg *cFileGenerator) generate() error {
@@ -44,7 +45,10 @@ func (cg *cFileGenerator) buildContent() (string, error) {
builder.WriteString(templateContent)
for _, fn := range cg.generator.Functions {
fnGen := PHPFuncGenerator{paramParser: &ParameterParser{}}
fnGen := PHPFuncGenerator{
paramParser: &ParameterParser{},
namespace: cg.generator.Namespace,
}
builder.WriteString(fnGen.generate(fn))
}
@@ -52,7 +56,10 @@ func (cg *cFileGenerator) buildContent() (string, error) {
}
func (cg *cFileGenerator) getTemplateContent() (string, error) {
tmpl := template.Must(template.New("cfile").Funcs(sprig.FuncMap()).Parse(cFileContent))
funcMap := sprig.FuncMap()
funcMap["namespacedClassName"] = NamespacedName
tmpl := template.Must(template.New("cfile").Funcs(funcMap).Parse(cFileContent))
var buf bytes.Buffer
if err := tmpl.Execute(&buf, cTemplateData{
@@ -60,6 +67,7 @@ func (cg *cFileGenerator) getTemplateContent() (string, error) {
Functions: cg.generator.Functions,
Classes: cg.generator.Classes,
Constants: cg.generator.Constants,
Namespace: cg.generator.Namespace,
}); err != nil {
return "", err
}

View File

@@ -0,0 +1,130 @@
package extgen
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
)
func TestNamespacedClassName(t *testing.T) {
tests := []struct {
name string
namespace string
className string
expected string
}{
{
name: "no namespace",
namespace: "",
className: "MySuperClass",
expected: "MySuperClass",
},
{
name: "single level namespace",
namespace: "MyNamespace",
className: "MySuperClass",
expected: "MyNamespace_MySuperClass",
},
{
name: "multi level namespace",
namespace: `Go\Extension`,
className: "MySuperClass",
expected: "Go_Extension_MySuperClass",
},
{
name: "deep namespace",
namespace: `My\Deep\Nested\Namespace`,
className: "TestClass",
expected: "My_Deep_Nested_Namespace_TestClass",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NamespacedName(tt.namespace, tt.className)
require.Equal(t, tt.expected, result, "expected %q, got %q", tt.expected, result)
})
}
}
func TestCFileGenerationWithNamespace(t *testing.T) {
content := `package main
//export_php:namespace Go\Extension
//export_php:class MySuperClass
type MySuperClass struct{}
//export_php:method MySuperClass test(): string
func (m *MySuperClass) Test() string {
return "test"
}
`
tmpfile, err := os.CreateTemp("", "test_cfile_namespace_*.go")
require.NoError(t, err, "Failed to create temp file")
defer func() {
err := os.Remove(tmpfile.Name())
assert.NoError(t, err, "Failed to remove temp file: %v", err)
}()
_, err = tmpfile.Write([]byte(content))
require.NoError(t, err, "Failed to write to temp file")
err = tmpfile.Close()
require.NoError(t, err, "Failed to close temp file")
generator := &Generator{
BaseName: "test_extension",
SourceFile: tmpfile.Name(),
BuildDir: t.TempDir(),
Namespace: `Go\Extension`,
Classes: []phpClass{
{
Name: "MySuperClass",
GoStruct: "MySuperClass",
Methods: []phpClassMethod{
{
Name: "test",
PhpName: "test",
Signature: "test(): string",
ReturnType: "string",
ClassName: "MySuperClass",
},
},
},
},
}
cFileGen := cFileGenerator{generator: generator}
contentResult, err := cFileGen.getTemplateContent()
require.NoError(t, err, "error generating C file")
expectedCall := "register_class_Go_Extension_MySuperClass()"
require.Contains(t, contentResult, expectedCall, "C file should contain the standard function call")
oldCall := "register_class_MySuperClass()"
require.NotContains(t, contentResult, oldCall, "C file should not contain old non-namespaced call")
}
func TestCFileGenerationWithoutNamespace(t *testing.T) {
generator := &Generator{
BaseName: "test_extension",
BuildDir: t.TempDir(),
Namespace: "",
Classes: []phpClass{
{
Name: "MySuperClass",
GoStruct: "MySuperClass",
},
},
}
cFileGen := cFileGenerator{generator: generator}
contentResult, err := cFileGen.getTemplateContent()
require.NoError(t, err, "error generating C file")
expectedCall := "register_class_MySuperClass()"
require.Contains(t, contentResult, expectedCall, "C file should not contain the standard function call")
}

View File

@@ -0,0 +1,186 @@
package extgen
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestCFile_NamespacedPHPMethods(t *testing.T) {
tests := []struct {
name string
namespace string
classes []phpClass
expected []string
}{
{
name: "no namespace - regular PHP_METHOD",
namespace: "",
classes: []phpClass{
{
Name: "TestClass",
GoStruct: "TestClass",
Methods: []phpClassMethod{
{Name: "testMethod", PhpName: "testMethod", ClassName: "TestClass"},
},
},
},
expected: []string{
"PHP_METHOD(TestClass, __construct)",
"PHP_METHOD(TestClass, testMethod)",
},
},
{
name: "single level namespace",
namespace: "MyNamespace",
classes: []phpClass{
{
Name: "TestClass",
GoStruct: "TestClass",
Methods: []phpClassMethod{
{Name: "testMethod", PhpName: "testMethod", ClassName: "TestClass"},
},
},
},
expected: []string{
"PHP_METHOD(MyNamespace_TestClass, __construct)",
"PHP_METHOD(MyNamespace_TestClass, testMethod)",
},
},
{
name: "multi level namespace",
namespace: `Go\Extension`,
classes: []phpClass{
{
Name: "MySuperClass",
GoStruct: "MySuperClass",
Methods: []phpClassMethod{
{Name: "getName", PhpName: "getName", ClassName: "MySuperClass"},
{Name: "setName", PhpName: "setName", ClassName: "MySuperClass"},
},
},
},
expected: []string{
"PHP_METHOD(Go_Extension_MySuperClass, __construct)",
"PHP_METHOD(Go_Extension_MySuperClass, getName)",
"PHP_METHOD(Go_Extension_MySuperClass, setName)",
},
},
{
name: "multiple classes with namespace",
namespace: `Go\Extension`,
classes: []phpClass{
{
Name: "ClassA",
GoStruct: "ClassA",
Methods: []phpClassMethod{
{Name: "methodA", PhpName: "methodA", ClassName: "ClassA"},
},
},
{
Name: "ClassB",
GoStruct: "ClassB",
Methods: []phpClassMethod{
{Name: "methodB", PhpName: "methodB", ClassName: "ClassB"},
},
},
},
expected: []string{
"PHP_METHOD(Go_Extension_ClassA, __construct)",
"PHP_METHOD(Go_Extension_ClassA, methodA)",
"PHP_METHOD(Go_Extension_ClassB, __construct)",
"PHP_METHOD(Go_Extension_ClassB, methodB)",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
generator := &Generator{
BaseName: "test_extension",
Namespace: tt.namespace,
Classes: tt.classes,
BuildDir: t.TempDir(),
}
cFileGen := cFileGenerator{generator: generator}
content, err := cFileGen.getTemplateContent()
require.NoError(t, err, "error generating C template content: %v", err)
for _, expected := range tt.expected {
require.Contains(t, content, expected, "Expected to find %q in C template content", expected)
}
if tt.namespace != "" {
for _, class := range tt.classes {
oldConstructor := "PHP_METHOD(" + class.Name + ", __construct)"
require.NotContains(t, content, oldConstructor, "Did not expect to find old constructor declaration %q in namespaced content", oldConstructor)
for _, method := range class.Methods {
oldMethod := "PHP_METHOD(" + class.Name + ", " + method.PhpName + ")"
require.NotContains(t, content, oldMethod, "Did not expect to find old method declaration %q in namespaced content", oldMethod)
}
}
}
})
}
}
func TestCFile_PHP_METHOD_Integration(t *testing.T) {
generator := &Generator{
BaseName: "test_extension",
Namespace: `Go\Extension`,
Functions: []phpFunction{
{Name: "testFunc", ReturnType: "void"},
},
Classes: []phpClass{
{
Name: "MySuperClass",
GoStruct: "MySuperClass",
Methods: []phpClassMethod{
{
Name: "getName",
PhpName: "getName",
ReturnType: "string",
ClassName: "MySuperClass",
},
{
Name: "setName",
PhpName: "setName",
ReturnType: "void",
ClassName: "MySuperClass",
Params: []phpParameter{
{Name: "name", PhpType: "string"},
},
},
},
},
},
BuildDir: t.TempDir(),
}
cFileGen := cFileGenerator{generator: generator}
fullContent, err := cFileGen.buildContent()
require.NoError(t, err, "error generating full C file: %v", err)
expectedDeclarations := []string{
"PHP_FUNCTION(Go_Extension_testFunc)",
"PHP_METHOD(Go_Extension_MySuperClass, __construct)",
"PHP_METHOD(Go_Extension_MySuperClass, getName)",
"PHP_METHOD(Go_Extension_MySuperClass, setName)",
}
for _, expected := range expectedDeclarations {
require.Contains(t, fullContent, expected, "Expected to find %q in full C file content", expected)
}
oldDeclarations := []string{
"PHP_FUNCTION(testFunc)",
"PHP_METHOD(MySuperClass, __construct)",
"PHP_METHOD(MySuperClass, getName)",
"PHP_METHOD(MySuperClass, setName)",
}
for _, old := range oldDeclarations {
require.NotContains(t, fullContent, old, "Did not expect to find old declaration %q in full C file content", old)
}
}

View File

@@ -14,6 +14,7 @@ type Generator struct {
Functions []phpFunction
Classes []phpClass
Constants []phpConstant
Namespace string
}
// EXPERIMENTAL
@@ -79,6 +80,12 @@ func (g *Generator) parseSource() error {
}
g.Constants = constants
ns, err := parser.ParseNamespace(g.SourceFile)
if err != nil {
return fmt.Errorf("parsing namespace: %w", err)
}
g.Namespace = ns
return nil
}

View File

@@ -0,0 +1,123 @@
package extgen
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
)
func TestNamespaceParser(t *testing.T) {
tests := []struct {
name string
content string
expected string
shouldError bool
}{
{
name: "basic namespace",
content: `package main
//export_php:namespace My\Test\Namespace
func main() {}`,
expected: `My\Test\Namespace`,
},
{
name: "namespace with spaces",
content: `package main
//export_php:namespace My\Test\Namespace
func main() {}`,
expected: `My\Test\Namespace`,
},
{
name: "no namespace",
content: `package main
func main() {}`,
expected: "",
},
{
name: "multiple namespaces should error",
content: `package main
//export_php:namespace First\Namespace
//export_php:namespace Second\Namespace
func main() {}`,
expected: "",
shouldError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test_namespace_*.go")
require.NoError(t, err, "Failed to create temp file")
defer func() {
err := os.Remove(tmpfile.Name())
assert.NoError(t, err, "Failed to remove temp file: %v", err)
}()
_, err = tmpfile.Write([]byte(tt.content))
require.NoError(t, err, "Failed to write to temp file")
err = tmpfile.Close()
require.NoError(t, err, "Failed to close temp file")
parser := NamespaceParser{}
result, err := parser.parse(tmpfile.Name())
if tt.shouldError {
require.Error(t, err, "expected error but got none")
return
}
require.NoError(t, err, "unexpected error")
require.Equal(t, tt.expected, result, "expected %q, got %q", tt.expected, result)
})
}
}
func TestGeneratorWithNamespace(t *testing.T) {
content := `package main
//export_php:namespace My\Test\Namespace
//export_php:function hello(): string
func hello() string {
return "Hello from namespace!"
}
//export_php:constant TEST_CONSTANT = "test_value"
const TEST_CONSTANT = "test_value"
`
tmpfile, err := os.CreateTemp("", "test_generator_namespace_*.go")
require.NoError(t, err, "Failed to create temp file")
defer func() {
if err := os.Remove(tmpfile.Name()); err != nil {
t.Logf("Failed to remove temp file: %v", err)
}
}()
_, err = tmpfile.Write([]byte(content))
require.NoError(t, err, "Failed to write to temp file")
err = tmpfile.Close()
require.NoError(t, err, "Failed to close temp file")
parser := SourceParser{}
namespace, err := parser.ParseNamespace(tmpfile.Name())
require.NoErrorf(t, err, "Failed to parse namespace from %s: %v", tmpfile.Name(), err)
require.Equal(t, `My\Test\Namespace`, namespace, "Namespace should match the parsed namespace")
generator := &Generator{
SourceFile: tmpfile.Name(),
Namespace: namespace,
}
require.Equal(t, `My\Test\Namespace`, generator.Namespace, "Namespace should match the parsed namespace")
}

View File

@@ -0,0 +1,45 @@
package extgen
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
type NamespaceParser struct{}
var namespaceRegex = regexp.MustCompile(`//\s*export_php:namespace\s+(.+)`)
func (np *NamespaceParser) parse(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer func() {
if err := file.Close(); err != nil {
fmt.Printf("Error closing file %s: %v\n", filename, err)
}
}()
var foundNamespace string
var lineNumber int
var foundLineNumber int
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lineNumber++
line := strings.TrimSpace(scanner.Text())
if matches := namespaceRegex.FindStringSubmatch(line); matches != nil {
namespace := strings.TrimSpace(matches[1])
if foundNamespace != "" {
return "", fmt.Errorf("multiple namespace declarations found: first at line %d, second at line %d", foundLineNumber, lineNumber)
}
foundNamespace = namespace
foundLineNumber = lineNumber
}
}
return foundNamespace, scanner.Err()
}

View File

@@ -19,3 +19,9 @@ func (p *SourceParser) ParseConstants(filename string) ([]phpConstant, error) {
constantParser := NewConstantParserWithDefRegex()
return constantParser.parse(filename)
}
// EXPERIMENTAL
func (p *SourceParser) ParseNamespace(filename string) (string, error) {
namespaceParser := NamespaceParser{}
return namespaceParser.parse(filename)
}

View File

@@ -7,6 +7,7 @@ import (
type PHPFuncGenerator struct {
paramParser *ParameterParser
namespace string
}
func (pfg *PHPFuncGenerator) generate(fn phpFunction) string {
@@ -14,7 +15,8 @@ func (pfg *PHPFuncGenerator) generate(fn phpFunction) string {
paramInfo := pfg.paramParser.analyzeParameters(fn.Params)
builder.WriteString(fmt.Sprintf("PHP_FUNCTION(%s)\n{\n", fn.Name))
funcName := NamespacedName(pfg.namespace, fn.Name)
builder.WriteString(fmt.Sprintf("PHP_FUNCTION(%s)\n{\n", funcName))
if decl := pfg.paramParser.generateParamDeclarations(fn.Params); decl != "" {
builder.WriteString(decl + "\n")

View File

@@ -0,0 +1,161 @@
package extgen
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestPHPFuncGenerator_NamespacedFunctions(t *testing.T) {
tests := []struct {
name string
namespace string
function phpFunction
expected string
}{
{
name: "no namespace",
namespace: "",
function: phpFunction{Name: "test_func", ReturnType: "int"},
expected: "PHP_FUNCTION(test_func)",
},
{
name: "single level namespace",
namespace: "MyNamespace",
function: phpFunction{Name: "test_func", ReturnType: "int"},
expected: "PHP_FUNCTION(MyNamespace_test_func)",
},
{
name: "multi level namespace",
namespace: `Go\Extension`,
function: phpFunction{Name: "multiply", ReturnType: "int"},
expected: "PHP_FUNCTION(Go_Extension_multiply)",
},
{
name: "deep namespace",
namespace: `My\Deep\Nested\Namespace`,
function: phpFunction{Name: "is_even", ReturnType: "bool"},
expected: "PHP_FUNCTION(My_Deep_Nested_Namespace_is_even)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
generator := PHPFuncGenerator{
paramParser: &ParameterParser{},
namespace: tt.namespace,
}
result := generator.generate(tt.function)
require.Contains(t, result, tt.expected, "Expected to find %q in generated PHP code, but didn't.\nGenerated:\n%s", tt.expected, result)
})
}
}
func TestGetNamespacedFunctionName(t *testing.T) {
tests := []struct {
name string
namespace string
functionName string
expected string
}{
{
name: "no namespace",
namespace: "",
functionName: "test_func",
expected: "test_func",
},
{
name: "single level namespace",
namespace: "MyNamespace",
functionName: "test_func",
expected: "MyNamespace_test_func",
},
{
name: "multi level namespace",
namespace: `Go\Extension`,
functionName: "multiply",
expected: "Go_Extension_multiply",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NamespacedName(tt.namespace, tt.functionName)
require.Equal(t, tt.expected, result, "Expected %q, got %q", tt.expected, result)
})
}
}
func TestCFileWithNamespacedPHPFunctions(t *testing.T) {
generator := &Generator{
BaseName: "test_extension",
Namespace: `Go\Extension`,
Functions: []phpFunction{
{
Name: "multiply",
ReturnType: "int",
Params: []phpParameter{
{Name: "a", PhpType: "int"},
{Name: "b", PhpType: "int"},
},
},
{
Name: "is_even",
ReturnType: "bool",
Params: []phpParameter{
{Name: "num", PhpType: "int"},
},
},
},
Classes: []phpClass{
{
Name: "MySuperClass",
GoStruct: "MySuperClass",
Methods: []phpClassMethod{
{
Name: "getName",
PhpName: "getName",
ReturnType: "string",
ClassName: "MySuperClass",
},
},
},
},
BuildDir: t.TempDir(),
}
cFileGen := cFileGenerator{generator: generator}
content, err := cFileGen.buildContent()
require.NoError(t, err, "error generating C file")
expectedFunctions := []string{
"PHP_FUNCTION(Go_Extension_multiply)",
"PHP_FUNCTION(Go_Extension_is_even)",
}
for _, expected := range expectedFunctions {
require.Contains(t, content, expected, "Expected to find %q in C file content", expected)
}
expectedMethods := []string{
"PHP_METHOD(Go_Extension_MySuperClass, __construct)",
"PHP_METHOD(Go_Extension_MySuperClass, getName)",
}
for _, expected := range expectedMethods {
require.Contains(t, content, expected, "Expected to find %q in C file content", expected)
}
oldDeclarations := []string{
"PHP_FUNCTION(multiply)",
"PHP_FUNCTION(is_even)",
"PHP_METHOD(MySuperClass, __construct)",
"PHP_METHOD(MySuperClass, getName)",
}
for _, old := range oldDeclarations {
require.NotContains(t, content, old, "Did not expect to find old declaration %q in C file content", old)
}
}

View File

@@ -59,7 +59,7 @@ void init_object_handlers() {
{{ range .Classes}}
static zend_class_entry *{{.Name}}_ce = NULL;
PHP_METHOD({{.Name}}, __construct) {
PHP_METHOD({{namespacedClassName $.Namespace .Name}}, __construct) {
ZEND_PARSE_PARAMETERS_NONE();
{{$.BaseName}}_object *intern = {{$.BaseName}}_object_from_obj(Z_OBJ_P(ZEND_THIS));
@@ -73,7 +73,7 @@ PHP_METHOD({{.Name}}, __construct) {
}
{{ range .Methods}}
PHP_METHOD({{.ClassName}}, {{.PhpName}}) {
PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
{{$.BaseName}}_object *intern = {{$.BaseName}}_object_from_obj(Z_OBJ_P(ZEND_THIS));
VALIDATE_GO_HANDLE(intern);
@@ -132,7 +132,7 @@ void register_all_classes() {
init_object_handlers();
{{- range .Classes}}
{{.Name}}_ce = register_class_{{.Name}}();
{{.Name}}_ce = register_class_{{namespacedClassName $.Namespace .Name}}();
if (!{{.Name}}_ce) {
php_error_docref(NULL, E_ERROR, "Failed to register class {{.Name}}");
return;

View File

@@ -1,7 +1,9 @@
<?php
/** @generate-class-entries */
{{if .Namespace}}
namespace {{.Namespace}};
{{end}}
{{range .Constants}}{{if eq .ClassName ""}}{{if .IsIota}}/**
* @var int
* @cvalue {{.Name}}

View File

@@ -19,6 +19,17 @@ func readFile(filename string) (string, error) {
return string(content), nil
}
// NamespacedName converts a namespace and name to a C-compatible format.
// E.g., namespace "Go\Extension" and name "MyClass" become "Go_Extension_MyClass".
// This symbol remains exported, so it's usable in templates.
func NamespacedName(namespace, name string) string {
if namespace == "" {
return name
}
namespacePart := strings.ReplaceAll(namespace, "\\", "_")
return namespacePart + "_" + name
}
// EXPERIMENTAL
func SanitizePackageName(name string) string {
sanitized := strings.ReplaceAll(name, "-", "_")

View File

@@ -0,0 +1,59 @@
package extgen
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestNamespacedName(t *testing.T) {
tests := []struct {
name string
namespace string
itemName string
expected string
}{
{
name: "no namespace",
namespace: "",
itemName: "TestItem",
expected: "TestItem",
},
{
name: "single level namespace",
namespace: "MyNamespace",
itemName: "TestItem",
expected: "MyNamespace_TestItem",
},
{
name: "multi level namespace",
namespace: `Go\Extension`,
itemName: "TestItem",
expected: "Go_Extension_TestItem",
},
{
name: "deep namespace",
namespace: `Very\Deep\Nested\Namespace`,
itemName: "MyItem",
expected: "Very_Deep_Nested_Namespace_MyItem",
},
{
name: "function name",
namespace: `Go\Extension`,
itemName: "multiply",
expected: "Go_Extension_multiply",
},
{
name: "class name",
namespace: `Go\Extension`,
itemName: "MySuperClass",
expected: "Go_Extension_MySuperClass",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NamespacedName(tt.namespace, tt.itemName)
require.Equal(t, tt.expected, result, "NamespacedName(%q, %q) = %q, expected %q", tt.namespace, tt.itemName, result, tt.expected)
})
}
}