├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── datatypes
└── datatypes.go
├── phpunit.xml.dist
├── src
├── CodeGenerator.php
├── IgnoreFileException.php
└── Type.php
└── tests
├── CodeGeneratorTest.php
├── fixtures
├── AbstractModel.php
├── AnotherModel.php
├── AnotherRootModel.php
├── IgnoredClass.php
├── InterfaceModel.php
└── RootModel.php
└── output
├── abstractmodel_generated.go
├── anothermodel_generated.go
├── anotherrootmodel_generated.go
└── rootmodel_generated.go
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | composer.lock
3 | /vendor/
4 | .php_cs.cache
5 |
6 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
7 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
8 | # composer.lock
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - '7.1'
4 |
5 | sudo: false
6 |
7 | before_install:
8 | - composer self-update
9 | install:
10 | - composer install
11 | script:
12 | - ./vendor/bin/php-cs-fixer fix . -v --dry-run --diff --using-cache=no
13 | - ./vendor/bin/phpunit --coverage-clover coverage.xml
14 | after_success:
15 | - travis_retry ./vendor/bin/php-coveralls -x coverage.xml -o coveralls.json
16 |
17 | cache:
18 | directories:
19 | - $HOME/.composer/cache/files
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Michael Weibel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # php-to-go
2 | [](https://travis-ci.org/mweibel/php-to-go)
3 | [](https://coveralls.io/github/mweibel/php-to-go?branch=master)
4 |
5 | Library for generating Go structs using [sheriff](https://github.com/liip/sheriff) out of PHP models which use [JMS Serializer](https://jmsyst.com/libs/serializer).
6 |
7 | ## Status
8 |
9 | Alpha.
10 |
11 | Has not been tested in real production workload yet. Has been tested locally against a test system using big models and quite some data.
12 |
13 | Documentation of what it does should become better too.
14 |
15 | ## Contributions
16 |
17 | Contributions in any form are welcome.
18 | I try to keep this library as small as possible. If you plan a big PR it might be better to ask first in an issue.
19 |
20 | If you change PHP code please ensure to accompany it with an automated test.
21 |
22 | ## Why
23 |
24 | Can be used to turn an existing serialization solution using PHP and JMS Serializer into one based on Go and sheriff.
25 |
26 | ## How
27 |
28 | ```php
29 | generate();
45 | ```
46 |
47 | The generated files can then be incorporated into any Go program.
48 |
49 | The code generator detects if there are methods annotated using `VirtualProperty`.
50 | In this case the generated model needs an AfterMarshal function receiver on that type.
51 | As the code generator will overwrite the files it generated (on repeated execution), customizations to the generated types
52 | should go into a separate file.
53 |
54 | Example noop `AfterMarshal` function on a type called `RootModel`:
55 |
56 | ```go
57 | package models
58 |
59 | import "github.com/liip/sheriff"
60 |
61 | func (rm RootModel) AfterMarshal(options *sheriff.Options, data interface{}) (interface{}, error) {
62 | return data, nil
63 | }
64 | ```
65 |
66 | If you want to interface with existing PHP code you can use e.g. [goridge](https://github.com/spiral/goridge).
67 |
68 | # License
69 |
70 | MIT (see LICENSE).
71 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mweibel/php-to-go",
3 | "description": "Utility to generate go types from php models",
4 | "minimum-stability": "stable",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Michael Weibel",
9 | "email": "michael.weibel@gmail.com"
10 | }
11 | ],
12 | "autoload": {
13 | "psr-4": {
14 | "PHPToGo\\": "src/"
15 | }
16 | },
17 | "autoload-dev": {
18 | "psr-4": {
19 | "PHPToGo\\Tests\\": "tests/"
20 | }
21 | },
22 | "extra": {
23 | "branch-alias": {
24 | "dev-master": "0.1.x-dev"
25 | }
26 | },
27 | "require": {
28 | "php": ">=7.1",
29 | "doctrine/annotations": "^1.3",
30 | "jms/serializer": "^1.4"
31 | },
32 | "require-dev": {
33 | "friendsofphp/php-cs-fixer": "^2.10",
34 | "phpunit/phpunit": "^7",
35 | "spatie/temporary-directory": "^1.1",
36 | "php-coveralls/php-coveralls": "^2.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/datatypes/datatypes.go:
--------------------------------------------------------------------------------
1 | package datatypes
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type DateTime time.Time
8 |
9 | func (dt *DateTime) UnmarshalJSON(data []byte) error {
10 | t, err := time.Parse(`"2006-01-02T15:04:05-0700"`, string(data))
11 | if err != nil {
12 | return err
13 | }
14 | *dt = DateTime(t)
15 | return nil
16 | }
17 |
18 | func (dt *DateTime) MarshalJSON() ([]byte, error) {
19 | t := time.Time(*dt)
20 | return []byte(t.Format(`"2006-01-02T15:04:05-0700"`)), nil
21 | }
22 |
23 | func (dt *DateTime) String() string {
24 | t := time.Time(*dt)
25 | return t.String()
26 | }
27 |
28 | type Date time.Time
29 |
30 | func (d *Date) UnmarshalJSON(data []byte) error {
31 | t, err := time.Parse(`"02.01.2006"`, string(data))
32 | if err != nil {
33 | return err
34 | }
35 | *d = Date(t)
36 | return nil
37 | }
38 |
39 | func (d *Date) MarshalJSON() ([]byte, error) {
40 | t := time.Time(*d)
41 | return []byte(t.Format(`"02.01.2006"`)), nil
42 | }
43 |
44 | func (d *Date) String() string {
45 | t := time.Time(*d)
46 | return t.String()
47 | }
48 |
49 | type IntlDate time.Time
50 |
51 | func (d *IntlDate) UnmarshalJSON(data []byte) error {
52 | t, err := time.Parse(`"2006-01-02"`, string(data))
53 | if err != nil {
54 | return err
55 | }
56 | *d = IntlDate(t)
57 | return nil
58 | }
59 |
60 | func (d *IntlDate) MarshalJSON() ([]byte, error) {
61 | t := time.Time(*d)
62 | return []byte(t.Format(`"2006-01-02"`)), nil
63 | }
64 |
65 | func (d *IntlDate) String() string {
66 | t := time.Time(*d)
67 | return t.String()
68 | }
69 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ./tests
7 |
8 |
9 |
10 |
11 |
12 | src/
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/CodeGenerator.php:
--------------------------------------------------------------------------------
1 | 'string',
22 | 'integer' => 'int',
23 | 'int' => 'int',
24 | 'boolean' => 'bool',
25 | 'float' => 'float64',
26 | 'DateTime' => 'datatypes.DateTime',
27 | 'Date' => 'datatypes.Date',
28 | 'DateTimeImmutable' => 'datatypes.Date',
29 | 'IntlDate' => 'datatypes.IntlDate',
30 | ];
31 |
32 | /**
33 | * @var string
34 | */
35 | private $srcGlob;
36 | /**
37 | * @var string
38 | */
39 | private $targetDirectory;
40 | /**
41 | * @var string Ignored files from input dir
42 | */
43 | private $ignoredFiles;
44 | /**
45 | * @var string[] a map of property names to ignore
46 | */
47 | private $ignoredPropertyNames;
48 | /**
49 | * @var bool
50 | */
51 | private $verbose;
52 | /**
53 | * @var string
54 | */
55 | private $packagePath;
56 | /**
57 | * @var IndexedReader
58 | */
59 | private $reader;
60 | /**
61 | * @var array
62 | */
63 | private $typeMap;
64 | /**
65 | * @var TypeParser
66 | */
67 | private $typeParser;
68 | /**
69 | * @var string[] List of known namespaces which contain types we parse.
70 | */
71 | private $knownNamespaces = [];
72 |
73 | /**
74 | * @param string $srcGlob Glob to find all source PHP files
75 | * @param string $targetDirectory Target directory within GOPATH
76 | * @param string $packageName Go package name of the generated files
77 | * @param array [$ignoredFiles] List of files to ignore within the target directory
78 | * @param array $ignoredPropertyNames List of property names of mdoels to ignore
79 | * @param bool $verbose Whether to echo some status during the generation
80 | */
81 | public function __construct(string $srcGlob, string $targetDirectory, string $packageName, array $ignoredFiles = [], array $ignoredPropertyNames = [], bool $verbose = true)
82 | {
83 | if (!is_dir($targetDirectory)) {
84 | throw new \InvalidArgumentException('targetDirectory needs to be a valid directory');
85 | }
86 | $this->srcGlob = $srcGlob;
87 | $this->targetDirectory = $targetDirectory;
88 | $this->packageName = $packageName;
89 | $this->ignoredFiles = $ignoredFiles;
90 |
91 | // convert simple array to map for easier lookup
92 | foreach ($ignoredPropertyNames as $name) {
93 | $this->ignoredPropertyNames[$name] = true;
94 | }
95 | $this->verbose = $verbose;
96 |
97 | $this->packagePath = $this->guessPackagePath($targetDirectory);
98 |
99 |
100 | $this->reader = new IndexedReader(new AnnotationReader());
101 | $this->typeMap = [];
102 | $this->typeParser = new TypeParser();
103 | }
104 |
105 | /**
106 | * Start code generation
107 | */
108 | public function generate()
109 | {
110 | $this->copyDataTypes();
111 |
112 | foreach (glob($this->srcGlob) as $file) {
113 | $ignore = false;
114 | foreach ($this->ignoredFiles as $ignoredFile) {
115 | if (false !== strpos($file, $ignoredFile)) {
116 | $ignore = true;
117 | break;
118 | }
119 | }
120 | if (!$ignore) {
121 | $this->generateFile($file);
122 | }
123 | }
124 |
125 | $exec = 'gofmt -w '.$this->targetDirectory;
126 | $this->log('Successfully wrote all models. Executing '.$exec);
127 | $retVal = shell_exec($exec);
128 | if (null !== $retVal) {
129 | $this->log($retVal);
130 | }
131 | }
132 |
133 | private function generateFile(string $fileName)
134 | {
135 | $this->log($fileName);
136 |
137 | try {
138 | $type = new Type($fileName);
139 | } catch (IgnoreFileException $e) {
140 | $this->log($e->getMessage());
141 | return;
142 | }
143 | // if a type has already been processed, ignore.
144 | if (isset($this->typeMap[$type->getFullClassName()])) {
145 | return;
146 | }
147 | $this->typeMap[$type->getFullClassName()] = $type;
148 | $this->knownNamespaces[$type->getNamespace()] = true;
149 | $this->generateModel($type);
150 | }
151 |
152 | /**
153 | * Guess Go package path based on target directory (i.e. minus $GOPATH should be the dir)
154 | *
155 | * @param string $dir
156 | * @return string
157 | */
158 | private function guessPackagePath(string $dir): string
159 | {
160 | $absolute = realpath($dir);
161 | $goPath = realpath(implode(DIRECTORY_SEPARATOR, [getenv('GOPATH'), 'src']));
162 | return str_replace($goPath.'/', '', $absolute);
163 | }
164 |
165 | /**
166 | * Reads the annotations and generates attributes out of it.
167 | *
168 | * @param Type $type
169 | */
170 | private function generateModel(Type $type)
171 | {
172 | $reflClass = new \ReflectionClass($type->getFullClassName());
173 | $attrs = [];
174 | $needsAfterMarshal = false;
175 |
176 | // needs after marshal is determined by having methods with a JMS\Serializer annotation in the model
177 | // These most likely have specific PHP code on how to serialize certain properties -> can't be auto translated at the moment.
178 | foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
179 | foreach ($this->reader->getMethodAnnotations($method) as $annotation) {
180 | $annotationClass = get_class($annotation);
181 | if (0 === strpos($annotationClass, 'JMS\Serializer')) {
182 | $needsAfterMarshal = true;
183 | break;
184 | }
185 | }
186 | }
187 |
188 | foreach ($reflClass->getProperties() as $property) {
189 | if (isset($this->ignoredPropertyNames[$property->getName()])) {
190 | continue;
191 | }
192 |
193 | $propertyAnnotations = [];
194 |
195 | foreach ($this->reader->getPropertyAnnotations($property) as $annotation) {
196 | $propertyAnnotations = array_merge($propertyAnnotations, $this->parsePropertyAnnotation($annotation));
197 | }
198 |
199 | if (count($propertyAnnotations)) {
200 | $attrs[$property->getName()] = $propertyAnnotations;
201 | }
202 | }
203 | $type->update($attrs, $needsAfterMarshal);
204 | $type->write($this->targetDirectory, $this->packageName, $this->packagePath);
205 | }
206 |
207 | /**
208 | * Parses an annotation.
209 | *
210 | * @param mixed $annotation
211 | * @return array
212 | */
213 | private function parsePropertyAnnotation($annotation): array
214 | {
215 | $propertyAnnotations = [];
216 | $annotationClass = get_class($annotation);
217 |
218 | if (0 === strpos($annotationClass, 'JMS\Serializer')) {
219 | $annotationType = strtolower(substr($annotationClass, strlen('JMS\Serializer\Annotation\\')));
220 | if ($annotationType === 'exclude') {
221 | return $propertyAnnotations;
222 | }
223 |
224 | $propertyAnnotations[$annotationType] = [];
225 |
226 | $annotationReflClass = new \ReflectionClass($annotationClass);
227 | $props = $annotationReflClass->getProperties();
228 | $propsCount = count($props);
229 |
230 | foreach ($annotationReflClass->getProperties() as $attrProperty) {
231 | $name = $attrProperty->getName();
232 | $value = $annotation->$name;
233 |
234 | if (!$value) {
235 | continue;
236 | }
237 | if (1 === $propsCount) {
238 | $propertyAnnotations[$annotationType] = $value;
239 | break;
240 | }
241 | $propertyAnnotations[$annotationType][$name] = $value;
242 | }
243 |
244 | if ('type' === $annotationType) {
245 | $originalType = $propertyAnnotations['type'];
246 | if ('array' === $originalType) {
247 | return [];
248 | }
249 |
250 | $newType = $this->parseType($this->typeParser->parse($originalType));
251 | $propertyAnnotations['type'] = $newType;
252 |
253 | return $propertyAnnotations;
254 | }
255 | }
256 |
257 | return $propertyAnnotations;
258 | }
259 |
260 | /**
261 | * Tries to figure out which Go type to use.
262 | *
263 | * @param array $type
264 | * @return string
265 | */
266 | private function parseType(array $type): string
267 | {
268 | switch (count($type['params'])) {
269 | case 0:
270 | $typ = $this->convertPHPToGoType($type['name']);
271 | if (null !== $typ) {
272 | return $typ;
273 | }
274 | $this->log(print_r($typ, true));
275 | throw new \RuntimeException("Unknown type '${type['name']}'");
276 | case 1:
277 | $param = $type['params'][0];
278 | if ($type['name'] === 'DateTime') {
279 | switch ($param) {
280 | case 'd.m.Y':
281 | return $this->convertPHPToGoType('Date');
282 | case 'Y-m-d':
283 | return $this->convertPHPToGoType('IntlDate');
284 | default:
285 | throw new \RuntimeException("Param type DateTime<".$param."> not implemented.");
286 | }
287 | }
288 | return "[]" . $this->parseType($param);
289 | case 2:
290 | return "map[" . $this->parseType($type['params'][0]) . "]" . $this->parseType($type['params'][1]);
291 | break;
292 | default:
293 | throw new \RuntimeException('More than 2 params for a type not implemented');
294 | }
295 | }
296 |
297 | /**
298 | * @param string $type
299 | * @return null|string
300 | */
301 | private function convertPHPToGoType(string $type): ?string
302 | {
303 | if (isset(self::PHP_TO_GO_TYPES[$type])) {
304 | return self::PHP_TO_GO_TYPES[$type];
305 | }
306 | foreach ($this->knownNamespaces as $ns => $ignore) {
307 | if (0 === strpos($type, $ns)) {
308 | // get classname without namespace
309 | return substr($type, strrpos($type, '\\')+1);
310 | }
311 | }
312 | return null;
313 | }
314 |
315 | /**
316 | * Uses echo to log if verbose is true.
317 | *
318 | * @param $str
319 | */
320 | private function log(string $str)
321 | {
322 | if ($this->verbose) {
323 | echo $str."\n";
324 | }
325 | }
326 |
327 | /**
328 | * Copies the required data types file.
329 | */
330 | private function copyDataTypes()
331 | {
332 | $dest = implode(DIRECTORY_SEPARATOR, [$this->targetDirectory, 'datatypes']);
333 | @mkdir($dest);
334 | $fileGlob = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'datatypes', '*.go']);
335 | foreach (glob($fileGlob) as $file) {
336 | @copy($file, $dest.DIRECTORY_SEPARATOR.basename($file));
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/src/IgnoreFileException.php:
--------------------------------------------------------------------------------
1 | fileName = $fileName;
47 | $content = file_get_contents($fileName);
48 | if (false === $content) {
49 | throw new \RuntimeException('Unable to read file '.$fileName);
50 | }
51 | $this->content = $content;
52 | $this->fullClassName = $this->extractFullQualifiedClassName();
53 | $last = strrpos($this->fullClassName, '\\');
54 | $this->className = substr($this->fullClassName, $last+1);
55 | $this->namespace = substr($this->fullClassName, 0, $last);
56 | }
57 |
58 | /**
59 | * Updates Type with parsed attributes.
60 | *
61 | * @param array $attrs
62 | * @param bool $needsAfterMarshal
63 | */
64 | public function update(array $attrs, bool $needsAfterMarshal)
65 | {
66 | $this->attrs = $attrs;
67 | $this->needsAfterMarshal = $needsAfterMarshal;
68 | }
69 |
70 | /**
71 | * @return string
72 | */
73 | public function getNamespace(): string
74 | {
75 | return $this->namespace;
76 | }
77 |
78 | /**
79 | * @return string
80 | */
81 | public function getFullClassName(): string
82 | {
83 | return $this->fullClassName;
84 | }
85 |
86 | /**
87 | * @return string
88 | */
89 | public function getClassName(): string
90 | {
91 | return $this->className;
92 | }
93 |
94 | /**
95 | * Writes file.
96 | *
97 | * @param string $targetDirectory
98 | * @param string $packageName
99 | * @param string $packagePath
100 | */
101 | public function write(string $targetDirectory, string $packageName, string $packagePath)
102 | {
103 | $path = implode(DIRECTORY_SEPARATOR, [$targetDirectory, strtolower($this->className) . '_generated.go']);
104 | $file = fopen($path, 'w');
105 |
106 | $this->writeHeader($file, $packageName, $packagePath);
107 | $this->writeStruct($file);
108 |
109 | fclose($file);
110 | }
111 |
112 | private function writeHeader($file, string $packageName, string $packagePath)
113 | {
114 | fwrite($file, sprintf("package %s\nimport (\"%s/datatypes\"\n", $packageName, $packagePath));
115 | fwrite($file, "\"github.com/liip/sheriff\")\n");
116 | }
117 |
118 | private function writeStruct($file)
119 | {
120 | fwrite($file, sprintf("type %s struct {\n", $this->className));
121 |
122 | foreach ($this->attrs as $field => $value) {
123 | $this->writeAttr($file, $field, $value);
124 | }
125 |
126 | fwrite($file, "\n}\n");
127 | fwrite($file, sprintf("func (data %s) Marshal(options *sheriff.Options) (interface{}, error) {\n", $this->className));
128 | fwrite($file, "dest, err := sheriff.Marshal(options, data)\n");
129 | fwrite($file, "if err != nil {\n");
130 | fwrite($file, "return nil, err\n");
131 | fwrite($file, "}\n"); // if err != nil
132 |
133 | if ($this->needsAfterMarshal) {
134 | fwrite($file, "return data.AfterMarshal(options, dest)\n");
135 | } else {
136 | fwrite($file, "return dest, nil\n");
137 | }
138 | fwrite($file, "}\n"); // func
139 | }
140 |
141 | private function writeAttr($file, string $field, array $value)
142 | {
143 | $fieldName = $value['serializedname'] ?? $this->camelToSnake($field);
144 | $tag = sprintf('json:"%s,omitempty" ', $fieldName);
145 | foreach ($value as $key => $valueValue) {
146 | if ($key !== 'type' && $key !== 'serializedname' && null !== $valueValue) {
147 | if (is_array($valueValue)) {
148 | $valueValue = implode(',', $valueValue);
149 | }
150 | $tag .= sprintf('%s:"%s" ', $key, $valueValue);
151 | }
152 | }
153 |
154 | if (false !== array_search($field, self::RESERVED_WORDS)) {
155 | $field .= 'Field';
156 | }
157 |
158 | if (!array_key_exists('type', $value)) {
159 | fwrite($file, "// Warning: The following property has no 'TYPE' annotation!! Check the model\n//");
160 | $value['type'] = 'UNKNOWN';
161 | }
162 |
163 | fwrite($file, sprintf("\t%s *%s `%s`\n", ucfirst($field), $value['type'], trim($tag)));
164 | }
165 |
166 | private function camelToSnake(string $str): string
167 | {
168 | return ltrim(strtolower(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $str)), '_');
169 | }
170 |
171 | /**
172 | * From: http://stackoverflow.com/questions/7153000/get-class-name-from-file
173 | * @return string
174 | */
175 | private function extractFullQualifiedClassName(): string
176 | {
177 | $tokens = token_get_all($this->content);
178 | $class = $namespace = '';
179 | $namespaceStarted = false;
180 | $classStarted = false;
181 |
182 | for ($i = 0, $l = count($tokens); $i < $l; $i++) {
183 | if ($tokens[$i] === ';') {
184 | $namespaceStarted = false;
185 | continue;
186 | }
187 | if ($tokens[$i] === '{') {
188 | $classStarted = false;
189 | return $namespace.$class;
190 | }
191 | switch ($tokens[$i][0]) {
192 | case T_NAMESPACE:
193 | $namespaceStarted = true;
194 | break;
195 | case T_CLASS:
196 | $classStarted = true;
197 | break;
198 | case T_EXTENDS:
199 | // fallthrough
200 | case T_IMPLEMENTS:
201 | $classStarted = false;
202 | return $namespace.$class;
203 | case T_STRING:
204 | if ($namespaceStarted) {
205 | $namespace .= $tokens[$i][1] . '\\';
206 | break;
207 | }
208 | if ($classStarted) {
209 | $class .= $tokens[$i][1];
210 | break;
211 | }
212 | break;
213 | case T_INTERFACE:
214 | throw new IgnoreFileException('Interfaces are ignored');
215 | }
216 | }
217 | throw new \RuntimeException('Should never reach that point.');
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/tests/CodeGeneratorTest.php:
--------------------------------------------------------------------------------
1 | tempDirectory = (new TemporaryDirectory())->create();
32 |
33 | $this->goPath = $this->tempDirectory->path('gopath');
34 | $this->targetDirectory = $this->goPath.'/src/github.com/mweibel/php-to-go-tests';
35 |
36 | @mkdir($this->targetDirectory, 0777, true);
37 |
38 | putenv('GOPATH='.realpath($this->goPath));
39 | }
40 |
41 | protected function tearDown()
42 | {
43 | parent::tearDown();
44 |
45 | $this->tempDirectory->delete();
46 | }
47 |
48 | public function testCodeGenerator()
49 | {
50 | $fixturePath = dirname(__FILE__).'/fixtures';
51 | $generator = new CodeGenerator($fixturePath.'/*.php', $this->targetDirectory, 'models', ['IgnoredClass.php'], ['ignoredPropertyName'], false);
52 | $generator->generate();
53 |
54 | $expectedDir = dirname(__FILE__).'/output';
55 | $files = [];
56 | foreach (glob($expectedDir.'/*.go') as $expectedFile) {
57 | $name = basename($expectedFile);
58 |
59 | $files[$name] = true;
60 |
61 | $this->assertFileEquals($expectedFile, $this->targetDirectory.'/'.$name);
62 | }
63 | foreach (glob($this->targetDirectory.'/*.go') as $actualFile) {
64 | $name = basename($actualFile);
65 | if (isset($files[$name])) {
66 | continue;
67 | }
68 |
69 | $files[$name] = true;
70 |
71 | $this->assertFileEquals($expectedDir.'/'.$name, $actualFile);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/fixtures/AbstractModel.php:
--------------------------------------------------------------------------------
1 | ")
13 | * @Serializer\Groups({"api"})
14 | */
15 | protected $someStringArray = [];
16 | }
17 |
--------------------------------------------------------------------------------
/tests/fixtures/AnotherModel.php:
--------------------------------------------------------------------------------
1 | ")
35 | * @Serializer\Groups({"api"})
36 | */
37 | public $anotherModelList = [];
38 |
39 | /**
40 | * @var int[]
41 | * @Serializer\Type("array")
42 | */
43 | public $intArray = [];
44 |
45 | /**
46 | * @var AnotherModel[]
47 | * @Serializer\Since("3")
48 | * @Serializer\Type("array")
49 | * @Serializer\Groups({"api"})
50 | */
51 | public $mapStringAnotherModel = [];
52 |
53 | /**
54 | * Whether the product is purchasable online (i.e. it has any link to a retailer).
55 | *
56 | * @var bool
57 | * @Serializer\Until("2")
58 | * @Serializer\Type("boolean")
59 | * @Serializer\Groups({"api"})
60 | */
61 | public $someBool;
62 |
63 | /**
64 | * @var int
65 | * @Serializer\Type("integer")
66 | */
67 | public $someInt = 0;
68 |
69 | /**
70 | * @var AnotherModel[][]
71 | * @Serializer\Type("array>")
72 | * @Serializer\Groups({"api"})
73 | */
74 | public $twoDimensionalAnotherModel = [];
75 |
76 | /**
77 | * @var string[]
78 | * @Serializer\Type("array")
79 | * @Serializer\Groups({"not-api"})
80 | * @Serializer\Accessor(getter="getCustomGetterOrNull")
81 | */
82 | public $customGetter = [];
83 |
84 | /**
85 | * @var float
86 | * @Serializer\Type("float")
87 | */
88 | public $someFloat = 1.0;
89 |
90 | /**
91 | * @var \DateTime
92 | * @Serializer\Type("DateTime")
93 | */
94 | public $someDateTime;
95 |
96 | /**
97 | * @var \DateTime
98 | * @Serializer\Type("DateTime<'d.m.Y'>")
99 | */
100 | public $someDate;
101 |
102 | /**
103 | * @var \DateTime
104 | * @Serializer\Type("DateTime<'Y-m-d'>")
105 | */
106 | public $someDateIntl;
107 |
108 | /**
109 | * @var string
110 | * @Serializer\Exclude
111 | */
112 | public $excludedField;
113 |
114 | /**
115 | * @Serializer\Since("3")
116 | * @Serializer\Type("PHPToGo\Tests\fixtures\AnotherModel")
117 | * @Serializer\Groups({"api"})
118 | * @Serializer\VirtualProperty
119 | * @Serializer\SerializedName("another_model")
120 | *
121 | * @return AnotherModel|null
122 | */
123 | public function getAnotherModelInV3()
124 | {
125 | return $this->anotherModel ?: null;
126 | }
127 |
128 | public function getId(): string
129 | {
130 | return $this->id;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tests/output/abstractmodel_generated.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/liip/sheriff"
5 | "github.com/mweibel/php-to-go-tests/datatypes"
6 | )
7 |
8 | type AbstractModel struct {
9 | SomeStringArray *[]string `json:"some_string_array,omitempty" until:"2" groups:"api"`
10 | }
11 |
12 | func (data AbstractModel) Marshal(options *sheriff.Options) (interface{}, error) {
13 | dest, err := sheriff.Marshal(options, data)
14 | if err != nil {
15 | return nil, err
16 | }
17 | return dest, nil
18 | }
19 |
--------------------------------------------------------------------------------
/tests/output/anothermodel_generated.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/liip/sheriff"
5 | "github.com/mweibel/php-to-go-tests/datatypes"
6 | )
7 |
8 | type AnotherModel struct {
9 | Id *string `json:"id,omitempty" groups:"api"`
10 | }
11 |
12 | func (data AnotherModel) Marshal(options *sheriff.Options) (interface{}, error) {
13 | dest, err := sheriff.Marshal(options, data)
14 | if err != nil {
15 | return nil, err
16 | }
17 | return dest, nil
18 | }
19 |
--------------------------------------------------------------------------------
/tests/output/anotherrootmodel_generated.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/liip/sheriff"
5 | "github.com/mweibel/php-to-go-tests/datatypes"
6 | )
7 |
8 | type AnotherRootModel struct {
9 | Id *string `json:"id,omitempty" groups:"api"`
10 | AnotherModel *AnotherModel `json:"another_model,omitempty" until:"2" groups:"not-api"`
11 | }
12 |
13 | func (data AnotherRootModel) Marshal(options *sheriff.Options) (interface{}, error) {
14 | dest, err := sheriff.Marshal(options, data)
15 | if err != nil {
16 | return nil, err
17 | }
18 | return dest, nil
19 | }
20 |
--------------------------------------------------------------------------------
/tests/output/rootmodel_generated.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/liip/sheriff"
5 | "github.com/mweibel/php-to-go-tests/datatypes"
6 | )
7 |
8 | type RootModel struct {
9 | Id *string `json:"id,omitempty" groups:"api"`
10 | AnotherModel *AnotherModel `json:"another_model,omitempty" until:"2" groups:"not-api"`
11 | StringSinceV3 *string `json:"string_since_v3,omitempty" since:"3" groups:"not-api"`
12 | AnotherModelList *[]AnotherModel `json:"another_model_list,omitempty" groups:"api"`
13 | IntArray *[]int `json:"int_array,omitempty"`
14 | MapStringAnotherModel *map[string]AnotherModel `json:"map_string_another_model,omitempty" since:"3" groups:"api"`
15 | SomeBool *bool `json:"some_bool,omitempty" until:"2" groups:"api"`
16 | SomeInt *int `json:"some_int,omitempty"`
17 | TwoDimensionalAnotherModel *[][]AnotherModel `json:"two_dimensional_another_model,omitempty" groups:"api"`
18 | CustomGetter *[]string `json:"custom_getter,omitempty" groups:"not-api" accessor:"getCustomGetterOrNull"`
19 | SomeFloat *float64 `json:"some_float,omitempty"`
20 | SomeDateTime *datatypes.DateTime `json:"some_date_time,omitempty"`
21 | SomeDate *datatypes.Date `json:"some_date,omitempty"`
22 | SomeDateIntl *datatypes.IntlDate `json:"some_date_intl,omitempty"`
23 | SomeStringArray *[]string `json:"some_string_array,omitempty" until:"2" groups:"api"`
24 | }
25 |
26 | func (data RootModel) Marshal(options *sheriff.Options) (interface{}, error) {
27 | dest, err := sheriff.Marshal(options, data)
28 | if err != nil {
29 | return nil, err
30 | }
31 | return data.AfterMarshal(options, dest)
32 | }
33 |
--------------------------------------------------------------------------------