From cc8cd2037a7c8a117ec880420d7cbfb8a1c181d9 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 3 Jul 2025 15:32:08 +0200 Subject: [PATCH] feat(extgen): add support for `//export_php:namespace` --- docs/extensions.md | 55 +++++++ docs/fr/extensions.md | 55 +++++++ internal/extgen/cfile.go | 12 +- internal/extgen/cfile_namespace_test.go | 130 +++++++++++++++ internal/extgen/cfile_phpmethod_test.go | 186 ++++++++++++++++++++++ internal/extgen/generator.go | 7 + internal/extgen/namespace_test.go | 123 ++++++++++++++ internal/extgen/nsparser.go | 45 ++++++ internal/extgen/parser.go | 6 + internal/extgen/phpfunc.go | 4 +- internal/extgen/phpfunc_namespace_test.go | 161 +++++++++++++++++++ internal/extgen/templates/extension.c.tpl | 6 +- internal/extgen/templates/stub.php.tpl | 4 +- internal/extgen/utils.go | 11 ++ internal/extgen/utils_namespace_test.go | 59 +++++++ 15 files changed, 857 insertions(+), 7 deletions(-) create mode 100644 internal/extgen/cfile_namespace_test.go create mode 100644 internal/extgen/cfile_phpmethod_test.go create mode 100644 internal/extgen/namespace_test.go create mode 100644 internal/extgen/nsparser.go create mode 100644 internal/extgen/phpfunc_namespace_test.go create mode 100644 internal/extgen/utils_namespace_test.go diff --git a/docs/extensions.md b/docs/extensions.md index 7e3b7bab64..2a666f4f81 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -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 +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: diff --git a/docs/fr/extensions.md b/docs/fr/extensions.md index 4b3b1f6ae3..5de4ecf278 100644 --- a/docs/fr/extensions.md +++ b/docs/fr/extensions.md @@ -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 +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 : diff --git a/internal/extgen/cfile.go b/internal/extgen/cfile.go index 693e699549..c1e20657f1 100644 --- a/internal/extgen/cfile.go +++ b/internal/extgen/cfile.go @@ -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 } diff --git a/internal/extgen/cfile_namespace_test.go b/internal/extgen/cfile_namespace_test.go new file mode 100644 index 0000000000..b5f7ee87c1 --- /dev/null +++ b/internal/extgen/cfile_namespace_test.go @@ -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") +} diff --git a/internal/extgen/cfile_phpmethod_test.go b/internal/extgen/cfile_phpmethod_test.go new file mode 100644 index 0000000000..1c952578a1 --- /dev/null +++ b/internal/extgen/cfile_phpmethod_test.go @@ -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) + } +} diff --git a/internal/extgen/generator.go b/internal/extgen/generator.go index c728e61e39..f3c31e816b 100644 --- a/internal/extgen/generator.go +++ b/internal/extgen/generator.go @@ -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 } diff --git a/internal/extgen/namespace_test.go b/internal/extgen/namespace_test.go new file mode 100644 index 0000000000..7b72828053 --- /dev/null +++ b/internal/extgen/namespace_test.go @@ -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") +} diff --git a/internal/extgen/nsparser.go b/internal/extgen/nsparser.go new file mode 100644 index 0000000000..c2604a9adc --- /dev/null +++ b/internal/extgen/nsparser.go @@ -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() +} diff --git a/internal/extgen/parser.go b/internal/extgen/parser.go index f6cb70a415..23ddc6d22d 100644 --- a/internal/extgen/parser.go +++ b/internal/extgen/parser.go @@ -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) +} diff --git a/internal/extgen/phpfunc.go b/internal/extgen/phpfunc.go index f369eacf0a..2fdf519fef 100644 --- a/internal/extgen/phpfunc.go +++ b/internal/extgen/phpfunc.go @@ -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") diff --git a/internal/extgen/phpfunc_namespace_test.go b/internal/extgen/phpfunc_namespace_test.go new file mode 100644 index 0000000000..6ba8855e55 --- /dev/null +++ b/internal/extgen/phpfunc_namespace_test.go @@ -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) + } +} diff --git a/internal/extgen/templates/extension.c.tpl b/internal/extgen/templates/extension.c.tpl index 19138ed921..35cf7f4684 100644 --- a/internal/extgen/templates/extension.c.tpl +++ b/internal/extgen/templates/extension.c.tpl @@ -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; diff --git a/internal/extgen/templates/stub.php.tpl b/internal/extgen/templates/stub.php.tpl index 9c50d17730..cd16115d99 100644 --- a/internal/extgen/templates/stub.php.tpl +++ b/internal/extgen/templates/stub.php.tpl @@ -1,7 +1,9 @@