diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 613683e8..5f8cbbe9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -93,6 +93,49 @@ jobs: if: matrix.php-versions == '8.5' run: go mod tidy -diff working-directory: caddy/ + integration-tests: + name: Integration Tests (Linux, PHP ${{ matrix.php-versions }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ["8.3", "8.4", "8.5"] + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: actions/setup-go@v6 + with: + go-version: "1.25" + cache-dependency-path: | + go.sum + caddy/go.sum + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + ini-file: development + coverage: none + tools: none + env: + phpts: ts + debug: true + - name: Install PHP development libraries + run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev + - name: Install xcaddy + run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + - name: Download PHP sources + run: | + PHP_VERSION=$(php -r "echo PHP_VERSION;") + wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz" + tar xzf "php-${PHP_VERSION}.tar.gz" + echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}" + - name: Set CGO flags + run: | + echo "CGO_CFLAGS=$(php-config --includes)" >> "${GITHUB_ENV}" + echo "CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)" >> "${GITHUB_ENV}" + - name: Run integration tests + working-directory: internal/extgen/ + run: go test -tags integration -v -timeout 30m tests-mac: name: Tests (macOS, PHP 8.5) runs-on: macos-latest @@ -106,7 +149,7 @@ jobs: with: go-version: "1.25" cache-dependency-path: | - go.sum + go.sum caddy/go.sum - uses: shivammathur/setup-php@v2 with: diff --git a/internal/extgen/arginfo.go b/internal/extgen/arginfo.go index ce33b494..7d6aa08a 100644 --- a/internal/extgen/arginfo.go +++ b/internal/extgen/arginfo.go @@ -29,7 +29,7 @@ func (ag *arginfoGenerator) generate() error { output, err := cmd.CombinedOutput() if err != nil { log.Print("gen_stub.php output:\n", string(output)) - return fmt.Errorf("running gen_stub script: %w", err) + return fmt.Errorf("running gen_stub script: %w\nOutput: %s", err, string(output)) } return ag.fixArginfoFile(stubFile) diff --git a/internal/extgen/integration_test.go b/internal/extgen/integration_test.go new file mode 100644 index 00000000..6e40d6ef --- /dev/null +++ b/internal/extgen/integration_test.go @@ -0,0 +1,683 @@ +//go:build integration + +package extgen + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testModuleName = "github.com/frankenphp/test-extension" +) + +type IntegrationTestSuite struct { + tempDir string + genStubScript string + xcaddyPath string + frankenphpPath string + phpConfigPath string + t *testing.T +} + +func setupTest(t *testing.T) *IntegrationTestSuite { + t.Helper() + + suite := &IntegrationTestSuite{t: t} + + suite.genStubScript = os.Getenv("GEN_STUB_SCRIPT") + if suite.genStubScript == "" { + suite.genStubScript = "/usr/local/src/php/build/gen_stub.php" + } + + if _, err := os.Stat(suite.genStubScript); os.IsNotExist(err) { + t.Error("GEN_STUB_SCRIPT not found. Integration tests require PHP sources. Set GEN_STUB_SCRIPT environment variable.") + } + + xcaddyPath, err := exec.LookPath("xcaddy") + if err != nil { + t.Error("xcaddy not found in PATH. Integration tests require xcaddy to build FrankenPHP.") + } + suite.xcaddyPath = xcaddyPath + + phpConfigPath, err := exec.LookPath("php-config") + if err != nil { + t.Error("php-config not found in PATH. Integration tests require PHP development headers.") + } + suite.phpConfigPath = phpConfigPath + + tempDir := t.TempDir() + suite.tempDir = tempDir + + return suite +} + +func (s *IntegrationTestSuite) createGoModule(sourceFile string) (string, error) { + s.t.Helper() + + moduleDir := filepath.Join(s.tempDir, "module") + if err := os.MkdirAll(moduleDir, 0o755); err != nil { + return "", fmt.Errorf("failed to create module directory: %w", err) + } + + // Get project root for replace directive + projectRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + return "", fmt.Errorf("failed to get project root: %w", err) + } + + goModContent := fmt.Sprintf(`module %s + +go 1.25 + +require github.com/dunglas/frankenphp v0.0.0 + +replace github.com/dunglas/frankenphp => %s +`, testModuleName, projectRoot) + + if err := os.WriteFile(filepath.Join(moduleDir, "go.mod"), []byte(goModContent), 0o644); err != nil { + return "", fmt.Errorf("failed to create go.mod: %w", err) + } + + sourceContent, err := os.ReadFile(sourceFile) + if err != nil { + return "", fmt.Errorf("failed to read source file: %w", err) + } + + targetFile := filepath.Join(moduleDir, filepath.Base(sourceFile)) + if err := os.WriteFile(targetFile, sourceContent, 0o644); err != nil { + return "", fmt.Errorf("failed to write source file: %w", err) + } + + return targetFile, nil +} + +func (s *IntegrationTestSuite) runExtensionInit(sourceFile string) error { + s.t.Helper() + + os.Setenv("GEN_STUB_SCRIPT", s.genStubScript) + + baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go")) + generator := Generator{ + BaseName: baseName, + SourceFile: sourceFile, + BuildDir: filepath.Dir(sourceFile), + } + + if err := generator.Generate(); err != nil { + return fmt.Errorf("generation failed: %w", err) + } + + return nil +} + +// compileFrankenPHP compiles FrankenPHP with the generated extension +func (s *IntegrationTestSuite) compileFrankenPHP(moduleDir string) (string, error) { + s.t.Helper() + + projectRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + return "", fmt.Errorf("failed to get project root: %w", err) + } + + cflags, err := exec.Command(s.phpConfigPath, "--includes").Output() + if err != nil { + return "", fmt.Errorf("failed to get PHP includes: %w", err) + } + + ldflags, err := exec.Command(s.phpConfigPath, "--ldflags").Output() + if err != nil { + return "", fmt.Errorf("failed to get PHP ldflags: %w", err) + } + + libs, err := exec.Command(s.phpConfigPath, "--libs").Output() + if err != nil { + return "", fmt.Errorf("failed to get PHP libs: %w", err) + } + + cgoCflags := strings.TrimSpace(string(cflags)) + cgoLdflags := strings.TrimSpace(string(ldflags)) + " " + strings.TrimSpace(string(libs)) + + outputBinary := filepath.Join(s.tempDir, "frankenphp") + + cmd := exec.Command( + s.xcaddyPath, + "build", + "--output", outputBinary, + "--with", "github.com/dunglas/frankenphp="+projectRoot, + "--with", "github.com/dunglas/frankenphp/caddy="+projectRoot+"/caddy", + "--with", testModuleName+"="+moduleDir, + ) + + cmd.Env = append(os.Environ(), + "CGO_ENABLED=1", + "CGO_CFLAGS="+cgoCflags, + "CGO_LDFLAGS="+cgoLdflags, + fmt.Sprintf("XCADDY_GO_BUILD_FLAGS=-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nowatcher"), + ) + + cmd.Dir = s.tempDir + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("xcaddy build failed: %w\nOutput: %s", err, string(output)) + } + + s.frankenphpPath = outputBinary + return outputBinary, nil +} + +func (s *IntegrationTestSuite) runPHPCode(phpCode string) (string, error) { + s.t.Helper() + + if s.frankenphpPath == "" { + return "", fmt.Errorf("FrankenPHP not compiled yet") + } + + phpFile := filepath.Join(s.tempDir, "test.php") + if err := os.WriteFile(phpFile, []byte(phpCode), 0o644); err != nil { + return "", fmt.Errorf("failed to create PHP file: %w", err) + } + + cmd := exec.Command(s.frankenphpPath, "php-cli", phpFile) + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("PHP execution failed: %w\nOutput: %s", err, string(output)) + } + + return string(output), nil +} + +// verifyPHPSymbols checks if PHP can find the exposed functions, classes, and constants +func (s *IntegrationTestSuite) verifyPHPSymbols(functions []string, classes []string, constants []string) error { + s.t.Helper() + + var checks []string + + for _, fn := range functions { + checks = append(checks, fmt.Sprintf("if (!function_exists('%s')) { echo 'MISSING_FUNCTION: %s'; exit(1); }", fn, fn)) + } + + for _, cls := range classes { + checks = append(checks, fmt.Sprintf("if (!class_exists('%s')) { echo 'MISSING_CLASS: %s'; exit(1); }", cls, cls)) + } + + for _, cnst := range constants { + checks = append(checks, fmt.Sprintf("if (!defined('%s')) { echo 'MISSING_CONSTANT: %s'; exit(1); }", cnst, cnst)) + } + + checks = append(checks, "echo 'OK';") + + phpCode := "getValue() !== 0) { + echo "FAIL: Counter initial value expected 0, got " . $counter->getValue(); + exit(1); +} + +$counter->increment(); +if ($counter->getValue() !== 1) { + echo "FAIL: Counter after increment expected 1, got " . $counter->getValue(); + exit(1); +} + +$counter->decrement(); +if ($counter->getValue() !== 0) { + echo "FAIL: Counter after decrement expected 0, got " . $counter->getValue(); + exit(1); +} + +$counter->setValue(10); +if ($counter->getValue() !== 10) { + echo "FAIL: Counter after setValue(10) expected 10, got " . $counter->getValue(); + exit(1); +} + +$newValue = $counter->addValue(5); +if ($newValue !== 15) { + echo "FAIL: Counter addValue(5) expected to return 15, got $newValue"; + exit(1); +} +if ($counter->getValue() !== 15) { + echo "FAIL: Counter value after addValue(5) expected 15, got " . $counter->getValue(); + exit(1); +} + +$counter->updateWithNullable(50); +if ($counter->getValue() !== 50) { + echo "FAIL: Counter after updateWithNullable(50) expected 50, got " . $counter->getValue(); + exit(1); +} + +$counter->updateWithNullable(null); +if ($counter->getValue() !== 50) { + echo "FAIL: Counter after updateWithNullable(null) expected 50 (unchanged), got " . $counter->getValue(); + exit(1); +} + +$counter->reset(); +if ($counter->getValue() !== 0) { + echo "FAIL: Counter after reset expected 0, got " . $counter->getValue(); + exit(1); +} + +$counter1 = new Counter(); +$counter2 = new Counter(); +$counter1->setValue(100); +$counter2->setValue(200); +if ($counter1->getValue() !== 100 || $counter2->getValue() !== 200) { + echo "FAIL: Multiple Counter instances should be independent"; + exit(1); +} + +$holder = new StringHolder(); +$holder->setData("test string"); +if ($holder->getData() !== "test string") { + echo "FAIL: StringHolder getData expected 'test string', got '" . $holder->getData() . "'"; + exit(1); +} +if ($holder->getLength() !== 11) { + echo "FAIL: StringHolder getLength expected 11, got " . $holder->getLength(); + exit(1); +} + +$holder->setData(""); +if ($holder->getData() !== "") { + echo "FAIL: StringHolder empty string expected '', got '" . $holder->getData() . "'"; + exit(1); +} +if ($holder->getLength() !== 0) { + echo "FAIL: StringHolder empty string length expected 0, got " . $holder->getLength(); + exit(1); +} + +echo "OK"; +`, "OK") + require.NoError(t, err, "all class methods should work correctly") +} + +func TestConstants(t *testing.T) { + suite := setupTest(t) + + sourceFile := filepath.Join("..", "..", "testdata", "integration", "constants.go") + sourceFile, err := filepath.Abs(sourceFile) + require.NoError(t, err) + + targetFile, err := suite.createGoModule(sourceFile) + require.NoError(t, err) + + err = suite.runExtensionInit(targetFile) + require.NoError(t, err) + + _, err = suite.compileFrankenPHP(filepath.Dir(targetFile)) + require.NoError(t, err) + + err = suite.verifyPHPSymbols( + []string{"test_with_constants"}, + []string{"Config"}, + []string{ + "TEST_MAX_RETRIES", "TEST_API_VERSION", "TEST_ENABLED", "TEST_PI", + "STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED", + }, + ) + require.NoError(t, err, "all constants, functions, and classes should be accessible from PHP") + + err = suite.verifyFunctionBehavior(` 0.00001) { + echo "FAIL: TEST_PI expected 3.14159, got " . TEST_PI; + exit(1); +} + +if (Config::MODE_DEBUG !== 1) { + echo "FAIL: Config::MODE_DEBUG expected 1, got " . Config::MODE_DEBUG; + exit(1); +} + +if (Config::MODE_PRODUCTION !== 2) { + echo "FAIL: Config::MODE_PRODUCTION expected 2, got " . Config::MODE_PRODUCTION; + exit(1); +} + +if (Config::DEFAULT_TIMEOUT !== 30) { + echo "FAIL: Config::DEFAULT_TIMEOUT expected 30, got " . Config::DEFAULT_TIMEOUT; + exit(1); +} + +$config = new Config(); +$config->setMode(Config::MODE_DEBUG); +if ($config->getMode() !== Config::MODE_DEBUG) { + echo "FAIL: Config getMode expected MODE_DEBUG, got " . $config->getMode(); + exit(1); +} + +$result = test_with_constants(STATUS_PENDING); +if ($result !== "pending") { + echo "FAIL: test_with_constants(STATUS_PENDING) expected 'pending', got '$result'"; + exit(1); +} + +$result = test_with_constants(STATUS_PROCESSING); +if ($result !== "processing") { + echo "FAIL: test_with_constants(STATUS_PROCESSING) expected 'processing', got '$result'"; + exit(1); +} + +$result = test_with_constants(STATUS_COMPLETED); +if ($result !== "completed") { + echo "FAIL: test_with_constants(STATUS_COMPLETED) expected 'completed', got '$result'"; + exit(1); +} + +$result = test_with_constants(999); +if ($result !== "unknown") { + echo "FAIL: test_with_constants(999) expected 'unknown', got '$result'"; + exit(1); +} + +echo "OK"; +`, "OK") + require.NoError(t, err, "all constants should have correct values and functions should work") +} + +func TestNamespace(t *testing.T) { + suite := setupTest(t) + + sourceFile := filepath.Join("..", "..", "testdata", "integration", "namespace.go") + sourceFile, err := filepath.Abs(sourceFile) + require.NoError(t, err) + + targetFile, err := suite.createGoModule(sourceFile) + require.NoError(t, err) + + err = suite.runExtensionInit(targetFile) + require.NoError(t, err) + + _, err = suite.compileFrankenPHP(filepath.Dir(targetFile)) + require.NoError(t, err) + + err = suite.verifyPHPSymbols( + []string{`\\TestIntegration\\Extension\\greet`}, + []string{`\\TestIntegration\\Extension\\Person`}, + []string{`\\TestIntegration\\Extension\\NAMESPACE_VERSION`}, + ) + require.NoError(t, err, "all namespaced symbols should be accessible from PHP") + + err = suite.verifyFunctionBehavior(`setName("Bob"); +$person->setAge(25); + +if ($person->getName() !== "Bob") { + echo "FAIL: Person getName expected 'Bob', got '" . $person->getName() . "'"; + exit(1); +} + +if ($person->getAge() !== 25) { + echo "FAIL: Person getAge expected 25, got " . $person->getAge(); + exit(1); +} + +$person->setAge(Extension\Person::DEFAULT_AGE); +if ($person->getAge() !== 18) { + echo "FAIL: Person setAge(DEFAULT_AGE) expected 18, got " . $person->getAge(); + exit(1); +} + +$person1 = new Extension\Person(); +$person2 = new Extension\Person(); +$person1->setName("Alice"); +$person1->setAge(30); +$person2->setName("Charlie"); +$person2->setAge(40); + +if ($person1->getName() !== "Alice" || $person1->getAge() !== 30) { + echo "FAIL: person1 should have independent state"; + exit(1); +} +if ($person2->getName() !== "Charlie" || $person2->getAge() !== 40) { + echo "FAIL: person2 should have independent state"; + exit(1); +} + +echo "OK"; +`, "OK") + require.NoError(t, err, "all namespaced symbols should work correctly") +} + +func TestInvalidSignature(t *testing.T) { + suite := setupTest(t) + + sourceFile := filepath.Join("..", "..", "testdata", "integration", "invalid_signature.go") + sourceFile, err := filepath.Abs(sourceFile) + require.NoError(t, err) + + targetFile, err := suite.createGoModule(sourceFile) + require.NoError(t, err) + + err = suite.runExtensionInit(targetFile) + assert.Error(t, err, "extension-init should fail for invalid return type") + assert.Contains(t, err.Error(), "no PHP functions, classes, or constants found", "invalid functions should be ignored, resulting in no valid exports") +} + +func TestTypeMismatch(t *testing.T) { + suite := setupTest(t) + + sourceFile := filepath.Join("..", "..", "testdata", "integration", "type_mismatch.go") + sourceFile, err := filepath.Abs(sourceFile) + require.NoError(t, err) + + targetFile, err := suite.createGoModule(sourceFile) + require.NoError(t, err) + + err = suite.runExtensionInit(targetFile) + assert.NoError(t, err, "generation should succeed - class is valid even though function/method have type mismatches") + + baseDir := filepath.Dir(targetFile) + baseName := strings.TrimSuffix(filepath.Base(targetFile), ".go") + stubFile := filepath.Join(baseDir, baseName+".stub.php") + assert.FileExists(t, stubFile, "stub file should be generated for valid class") +} + +func TestMissingGenStub(t *testing.T) { + // temp override of GEN_STUB_SCRIPT + originalValue := os.Getenv("GEN_STUB_SCRIPT") + defer os.Setenv("GEN_STUB_SCRIPT", originalValue) + + os.Setenv("GEN_STUB_SCRIPT", "/nonexistent/gen_stub.php") + + tempDir := t.TempDir() + sourceFile := filepath.Join(tempDir, "test.go") + + err := os.WriteFile(sourceFile, []byte(`package test + +//export_php:function dummy(): void +func dummy() {} +`), 0o644) + require.NoError(t, err) + + baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go")) + gen := Generator{ + BaseName: baseName, + SourceFile: sourceFile, + BuildDir: filepath.Dir(sourceFile), + } + + err = gen.Generate() + assert.Error(t, err, "should fail when gen_stub.php is missing") + assert.Contains(t, err.Error(), "gen_stub.php", "error should mention missing script") +} diff --git a/testdata/integration/basic_function.go b/testdata/integration/basic_function.go new file mode 100644 index 00000000..50047646 --- /dev/null +++ b/testdata/integration/basic_function.go @@ -0,0 +1,32 @@ +package testintegration + +// #include +import "C" +import ( + "strings" + "unsafe" + + "github.com/dunglas/frankenphp" +) + +// export_php:function test_uppercase(string $str): string +func test_uppercase(s *C.zend_string) unsafe.Pointer { + str := frankenphp.GoString(unsafe.Pointer(s)) + upper := strings.ToUpper(str) + return frankenphp.PHPString(upper, false) +} + +// export_php:function test_add_numbers(int $a, int $b): int +func test_add_numbers(a int64, b int64) int64 { + return a + b +} + +// export_php:function test_multiply(float $a, float $b): float +func test_multiply(a float64, b float64) float64 { + return a * b +} + +// export_php:function test_is_enabled(bool $flag): bool +func test_is_enabled(flag bool) bool { + return !flag +} diff --git a/testdata/integration/class_methods.go b/testdata/integration/class_methods.go new file mode 100644 index 00000000..e84d346a --- /dev/null +++ b/testdata/integration/class_methods.go @@ -0,0 +1,72 @@ +package testintegration + +// #include +import "C" +import ( + "unsafe" + + "github.com/dunglas/frankenphp" +) + +// export_php:class Counter +type CounterStruct struct { + Value int +} + +// export_php:method Counter::increment(): void +func (c *CounterStruct) Increment() { + c.Value++ +} + +// export_php:method Counter::decrement(): void +func (c *CounterStruct) Decrement() { + c.Value-- +} + +// export_php:method Counter::getValue(): int +func (c *CounterStruct) GetValue() int64 { + return int64(c.Value) +} + +// export_php:method Counter::setValue(int $value): void +func (c *CounterStruct) SetValue(value int64) { + c.Value = int(value) +} + +// export_php:method Counter::reset(): void +func (c *CounterStruct) Reset() { + c.Value = 0 +} + +// export_php:method Counter::addValue(int $amount): int +func (c *CounterStruct) AddValue(amount int64) int64 { + c.Value += int(amount) + return int64(c.Value) +} + +// export_php:method Counter::updateWithNullable(?int $newValue): void +func (c *CounterStruct) UpdateWithNullable(newValue *int64) { + if newValue != nil { + c.Value = int(*newValue) + } +} + +// export_php:class StringHolder +type StringHolderStruct struct { + Data string +} + +// export_php:method StringHolder::setData(string $data): void +func (sh *StringHolderStruct) SetData(data *C.zend_string) { + sh.Data = frankenphp.GoString(unsafe.Pointer(data)) +} + +// export_php:method StringHolder::getData(): string +func (sh *StringHolderStruct) GetData() unsafe.Pointer { + return frankenphp.PHPString(sh.Data, false) +} + +// export_php:method StringHolder::getLength(): int +func (sh *StringHolderStruct) GetLength() int64 { + return int64(len(sh.Data)) +} diff --git a/testdata/integration/constants.go b/testdata/integration/constants.go new file mode 100644 index 00000000..9bb6bab9 --- /dev/null +++ b/testdata/integration/constants.go @@ -0,0 +1,70 @@ +package testintegration + +// #include +import "C" +import ( + "unsafe" + + "github.com/dunglas/frankenphp" +) + +// export_php:const +const TEST_MAX_RETRIES = 100 + +// export_php:const +const TEST_API_VERSION = "2.0.0" + +// export_php:const +const TEST_ENABLED = true + +// export_php:const +const TEST_PI = 3.14159 + +// export_php:const +const STATUS_PENDING = iota + +// export_php:const +const STATUS_PROCESSING = iota + +// export_php:const +const STATUS_COMPLETED = iota + +// export_php:class Config +type ConfigStruct struct { + Mode int +} + +// export_php:classconst Config +const MODE_DEBUG = 1 + +// export_php:classconst Config +const MODE_PRODUCTION = 2 + +// export_php:classconst Config +const DEFAULT_TIMEOUT = 30 + +// export_php:method Config::setMode(int $mode): void +func (c *ConfigStruct) SetMode(mode int64) { + c.Mode = int(mode) +} + +// export_php:method Config::getMode(): int +func (c *ConfigStruct) GetMode() int64 { + return int64(c.Mode) +} + +// export_php:function test_with_constants(int $status): string +func test_with_constants(status int64) unsafe.Pointer { + var result string + switch status { + case STATUS_PENDING: + result = "pending" + case STATUS_PROCESSING: + result = "processing" + case STATUS_COMPLETED: + result = "completed" + default: + result = "unknown" + } + return frankenphp.PHPString(result, false) +} diff --git a/testdata/integration/invalid_signature.go b/testdata/integration/invalid_signature.go new file mode 100644 index 00000000..9c5ada97 --- /dev/null +++ b/testdata/integration/invalid_signature.go @@ -0,0 +1,9 @@ +package testintegration + +// #include +import "C" + +// export_php:function invalid_return_type(string $str): unsupported_type +func invalid_return_type(s *C.zend_string) int { + return 42 +} diff --git a/testdata/integration/namespace.go b/testdata/integration/namespace.go new file mode 100644 index 00000000..0391a5a6 --- /dev/null +++ b/testdata/integration/namespace.go @@ -0,0 +1,50 @@ +package testintegration + +// export_php:namespace TestIntegration\Extension + +// #include +import "C" +import ( + "unsafe" + + "github.com/dunglas/frankenphp" +) + +// export_php:const +const NAMESPACE_VERSION = "1.0.0" + +// export_php:function greet(string $name): string +func greet(name *C.zend_string) unsafe.Pointer { + str := frankenphp.GoString(unsafe.Pointer(name)) + result := "Hello, " + str + "!" + return frankenphp.PHPString(result, false) +} + +// export_php:class Person +type PersonStruct struct { + Name string + Age int +} + +// export_php:method Person::setName(string $name): void +func (p *PersonStruct) SetName(name *C.zend_string) { + p.Name = frankenphp.GoString(unsafe.Pointer(name)) +} + +// export_php:method Person::getName(): string +func (p *PersonStruct) GetName() unsafe.Pointer { + return frankenphp.PHPString(p.Name, false) +} + +// export_php:method Person::setAge(int $age): void +func (p *PersonStruct) SetAge(age int64) { + p.Age = int(age) +} + +// export_php:method Person::getAge(): int +func (p *PersonStruct) GetAge() int64 { + return int64(p.Age) +} + +// export_php:classconst Person +const DEFAULT_AGE = 18 diff --git a/testdata/integration/type_mismatch.go b/testdata/integration/type_mismatch.go new file mode 100644 index 00000000..8cc0157f --- /dev/null +++ b/testdata/integration/type_mismatch.go @@ -0,0 +1,19 @@ +package testintegration + +// #include +import "C" + +// export_php:function mismatched_param_type(int $value): int +func mismatched_param_type(value string) int64 { + return 0 +} + +// export_php:class BadClass +type BadClassStruct struct { + Value int +} + +// export_php:method BadClass::wrongReturnType(): string +func (bc *BadClassStruct) WrongReturnType() int { + return bc.Value +}