mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
feat(extgen): add support for //export_php:namespace (#1721)
This commit is contained in:
committed by
GitHub
parent
63c742648d
commit
1804e36b93
@@ -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:
|
||||
|
||||
@@ -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 :
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
130
internal/extgen/cfile_namespace_test.go
Normal file
130
internal/extgen/cfile_namespace_test.go
Normal 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")
|
||||
}
|
||||
186
internal/extgen/cfile_phpmethod_test.go
Normal file
186
internal/extgen/cfile_phpmethod_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
123
internal/extgen/namespace_test.go
Normal file
123
internal/extgen/namespace_test.go
Normal 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")
|
||||
}
|
||||
45
internal/extgen/nsparser.go
Normal file
45
internal/extgen/nsparser.go
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
161
internal/extgen/phpfunc_namespace_test.go
Normal file
161
internal/extgen/phpfunc_namespace_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
{{if .Namespace}}
|
||||
namespace {{.Namespace}};
|
||||
{{end}}
|
||||
{{range .Constants}}{{if eq .ClassName ""}}{{if .IsIota}}/**
|
||||
* @var int
|
||||
* @cvalue {{.Name}}
|
||||
|
||||
@@ -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, "-", "_")
|
||||
|
||||
59
internal/extgen/utils_namespace_test.go
Normal file
59
internal/extgen/utils_namespace_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user