├── .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 | [![Build Status](https://travis-ci.org/mweibel/php-to-go.svg?branch=master)](https://travis-ci.org/mweibel/php-to-go) 3 | [![Coverage Status](https://coveralls.io/repos/github/mweibel/php-to-go/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------