-
-
Save alt-st/c7ce345e7b5302de169e60b841e2e6fa to your computer and use it in GitHub Desktop.
Swag It: Reverse engineer Swagger-PHP annotations from an existing JSON payload
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// ------------------------------------------------------------------------------------- | |
// Tool to generate Swagger-PHP PHP 8 Attributes from JSON. | |
// Paste your JSON into the input field to generate the code. | |
// Inspired by: https://github.com/Roger13/SwagDefGen | |
// How to run: | |
// php -S localhost:8888 -t . | |
// http://localhost:8888/swagit-attrs.php | |
// ------------------------------------------------------------------------------------- | |
// --- Input Parameters --- | |
$tabSize = (int) filter_var($_REQUEST['tabSize'] ?? 4, FILTER_VALIDATE_INT, ['options' => ['default' => 4, 'min_range' => 1, 'max_range' => 255]]); | |
$tabInit = (int) filter_var($_REQUEST['tabInit'] ?? 0, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 0, 'max_range' => 255]]); | |
$input = ($_REQUEST['json'] ?? ''); | |
$output = ''; // Initialize output | |
// ------------------------------------------------------------------------------------- | |
// Class for converting JSON to Attributes | |
// ------------------------------------------------------------------------------------- | |
class SwagIt | |
{ | |
private int $tabSize; | |
private int $tabInit; | |
private string $baseTabs; // Base indentation (tabInit) | |
private string $indentTabs; // Indentation for one level (tabSize) | |
public function __construct(int $tabSize = 4, int $tabInit = 0) | |
{ | |
$this->tabSize = $tabSize; | |
$this->tabInit = $tabInit; | |
$this->baseTabs = str_repeat(' ', $this->tabInit); | |
$this->indentTabs = str_repeat(' ', $this->tabSize); | |
} | |
// --- Get indentation string for the current level --- | |
private function getTabs(int $level): string | |
{ | |
// Ensure level is not negative | |
$level = max(0, $level); | |
return $this->baseTabs . str_repeat($this->indentTabs, $level); | |
} | |
// --- Main conversion method --- | |
public function convert(mixed $jsonData): string | |
{ | |
if (is_array($jsonData)) { | |
if ($this->isAssoc($jsonData)) { | |
// Input is an OBJECT -> generate list of properties | |
// Level 0 for the outermost property list | |
return $this->convertObj($jsonData, 0); | |
} else { | |
// Input is an ARRAY -> generate content for OA\Items | |
// Level 0 for the outer type/items | |
$currentTabs = $this->getTabs(0); | |
$itemsContent = $this->convertArrayItems($jsonData, 1); // Items content at level 1 | |
$itemsTabs = $this->getTabs(1); // Indentation for items content | |
// Construct the string for array type with items | |
return "type: \"array\",\n" | |
. $currentTabs . "items: new OA\Items(" . ($itemsContent ? "\n" . $itemsContent . "\n" . $itemsTabs : '') . ")"; | |
} | |
} elseif (is_object($jsonData)) { | |
// Handle stdClass objects from json_decode without true flag | |
return $this->convertObj((array) $jsonData, 0); | |
} else { | |
return 'Error: Input data must be a JSON object or array.'; | |
} | |
} | |
// --- Check if an array is associative (JSON object) --- | |
private function isAssoc(array $arr): bool | |
{ | |
if ([] === $arr) return false; // Treat empty array as non-associative (JSON array) | |
return array_keys($arr) !== range(0, count($arr) - 1); | |
} | |
// --- Format example value for PHP code --- | |
private function formatExample(mixed $value): string | |
{ | |
// Use var_export for safe representation of the value in PHP code | |
return var_export($value, true); | |
} | |
// --- Convert JSON object to a string containing a list of "new OA\Property(...)," --- | |
private function convertObj(array $jsonNodes, int $level): string | |
{ | |
$propertiesOutput = []; | |
$propertyTabs = $this->getTabs($level); // Indentation for new OA\Property(...) | |
$parameterTabs = $this->getTabs($level + 1); // Indentation for parameters inside | |
foreach ($jsonNodes as $key => $value) { | |
$currentPropertyParams = []; // Parameters for the current new OA\Property | |
// Add 'property' parameter | |
$currentPropertyParams[] = "\n" . $parameterTabs . "property: " . var_export($key, true); | |
// Add other parameters (type, format, example, nested items/properties) | |
$itemParams = $this->convertItem($value, true, $level + 1); // Parameters are at the next level | |
if (!empty($itemParams)) { | |
$currentPropertyParams = array_merge($currentPropertyParams, $itemParams); | |
} | |
// Assemble the string for new OA\Property | |
// Parameters are joined by comma | |
$propertyString = $propertyTabs . "new OA\Property(" . implode(',', $currentPropertyParams); | |
// Add closing parenthesis at the same level as new OA\Property | |
$propertyString .= "\n" . $propertyTabs . ")"; | |
$propertiesOutput[] = $propertyString; | |
} | |
// Join all "new OA\Property(...)" strings with a comma and newline | |
return implode(",\n", $propertiesOutput); | |
} | |
/** | |
* Converts a JSON element value into an array of parameter strings for an OpenAPI attribute. | |
* | |
* @param mixed $value The value to convert. | |
* @param bool $isForProperty Flag indicating if parameters are for OA\Property (true) or OA\Items (false). | |
* @param int $level Current indentation level for the generated parameters. | |
* @return array Array of parameter strings (e.g., 'type: "string"'). | |
*/ | |
private function convertItem(mixed $value, bool $isForProperty, int $level): array | |
{ | |
$outputParams = []; | |
$currentTabs = $this->getTabs($level); // Indentation for this element's parameters | |
if (is_int($value)) { | |
$outputParams[] = "\n" . $currentTabs . "type: \"integer\""; | |
// Determine int32 or int64 based on value size | |
$outputParams[] = "\n" . $currentTabs . "format: \"" . (abs($value) > 2147483647 ? 'int64' : 'int32') . "\""; | |
$outputParams[] = "\n" . $currentTabs . "example: " . $this->formatExample($value); | |
} elseif (is_float($value)) { | |
$outputParams[] = "\n" . $currentTabs . "type: \"number\""; | |
$outputParams[] = "\n" . $currentTabs . "format: \"float\""; // Could also be "double" | |
$outputParams[] = "\n" . $currentTabs . "example: " . $this->formatExample($value); | |
} elseif (is_bool($value)) { | |
$outputParams[] = "\n" . $currentTabs . "type: \"boolean\""; | |
$outputParams[] = "\n" . $currentTabs . "example: " . $this->formatExample($value); | |
} elseif (is_null($value)) { | |
// Add nullable: true only for properties, not typically for items themselves | |
if ($isForProperty) { | |
$outputParams[] = "\n" . $currentTabs . "nullable: true"; | |
} | |
// Provide a default type even if the value is null | |
$outputParams[] = "\n" . $currentTabs . "type: \"string\""; // Default type assumption | |
$outputParams[] = "\n" . $currentTabs . "example: null"; | |
} elseif (is_string($value)) { | |
// Get type, format, and example for string | |
$stringParams = $this->convertStringParams($value, $level); // Pass level | |
$outputParams = array_merge($outputParams, $stringParams); | |
} elseif (is_array($value)) { | |
if ($this->isAssoc($value)) { | |
// --- Nested Object --- | |
$outputParams[] = "\n" . $currentTabs . "type: \"object\""; | |
// Recursively convert nested object properties at the next level | |
$nestedProperties = $this->convertObj($value, $level + 1); | |
if (trim($nestedProperties) !== '') { | |
// Wrap nested properties in properties: [ ... ] | |
$outputParams[] = "\n" . $currentTabs . "properties: [\n" . $nestedProperties . "\n" . $currentTabs . "]"; | |
} else { | |
// Handle empty nested object | |
$outputParams[] = "\n" . $currentTabs . "properties: []"; | |
} | |
} else { | |
// --- Nested Array --- | |
$outputParams[] = "\n" . $currentTabs . "type: \"array\""; | |
// Recursively convert array items content at the next level | |
$itemsContent = $this->convertArrayItems($value, $level + 1); | |
$itemsTabs = $this->getTabs($level + 1); // Indentation for items content | |
$closingItemsTabs = $this->getTabs($level); // Indentation for OA\Items closing parenthesis | |
if (trim($itemsContent) !== '') { | |
// Wrap items content in items: new OA\Items( ... ) | |
$outputParams[] = "\n" . $currentTabs . "items: new OA\Items(\n" . $itemsContent . "\n" . $itemsTabs . ")"; | |
} else { | |
// Handle empty nested array | |
$outputParams[] = "\n" . $currentTabs . "items: new OA\Items()"; // Empty items definition | |
} | |
} | |
} else { | |
// Fallback for unknown types (resource, etc.) - unlikely from JSON | |
$outputParams[] = "\n" . $currentTabs . "type: \"string\""; | |
$outputParams[] = "\n" . $currentTabs . "description: \"Unknown data type encountered\""; | |
} | |
return $outputParams; | |
} | |
/** | |
* Handles string type detection (date, date-time) and example. | |
* Returns an array of parameter strings (like 'type: "string"', 'format: "date"'). | |
*/ | |
private function convertStringParams(string $value, int $level): array | |
{ | |
$stringParams = []; | |
$currentTabs = $this->getTabs($level); | |
// Determine type and format based on string content | |
if (preg_match('/^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/', $value)) { | |
// Date format | |
$stringParams[] = "\n" . $currentTabs . "type: \"string\""; | |
$stringParams[] = "\n" . $currentTabs . "format: \"date\""; | |
} elseif (preg_match('/^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])[T ]([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.\d+)?(Z|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?$/i', $value)) { | |
// Date-time format (improved regex for ISO 8601 variations) | |
$stringParams[] = "\n" . $currentTabs . "type: \"string\""; | |
$stringParams[] = "\n" . $currentTabs . "format: \"date-time\""; | |
} else { | |
// Default string type | |
$stringParams[] = "\n" . $currentTabs . "type: \"string\""; | |
} | |
// Add example if the string is not empty | |
if ($value !== '') { | |
$stringParams[] = "\n" . $currentTabs . "example: " . $this->formatExample($value); | |
} | |
// Else: example is omitted for empty strings | |
return $stringParams; | |
} | |
/** | |
* Generates the *content* parameters string for OA\Items based on the first element. | |
* Example return: "\n type: \"object\",\n properties: [ ... ]" | |
*/ | |
private function convertArrayItems(array $value, int $level): string | |
{ | |
$itemsContentParams = []; | |
$currentTabs = $this->getTabs($level); // Indentation for items parameters | |
if (!empty($value)) { | |
// --- IMPORTANT: Only analyzes the FIRST element --- | |
$firstItem = $value[0]; | |
// Get the parameter array describing the first item | |
$itemsContentParams = $this->convertItem($firstItem, false, $level); // false = generating for Items | |
// TODO: Future improvement: Analyze more items for mixed types (oneOf, anyOf). | |
} else { | |
// Handle empty array - define items as a generic type, e.g., string. | |
$itemsContentParams[] = "\n" . $currentTabs . "type: \"string\" /* Default type for items in empty array */"; | |
} | |
// Join parameters into a single string | |
return implode(',', $itemsContentParams); | |
} | |
} | |
// ------------------------------------------------------------------------------------- | |
// Main Processing Logic (Handles form input and calls SwagIt) | |
// ------------------------------------------------------------------------------------- | |
if (!empty($input)) { | |
// Decode JSON - IMPORTANT: use true for associative arrays | |
$json = json_decode($input, true); | |
if (json_last_error() === JSON_ERROR_NONE) { | |
$swagIt = new SwagIt($tabSize, $tabInit); | |
$generatedCode = $swagIt->convert($json); | |
// Assign the generated code directly to output (no introductory comment) | |
$output = $generatedCode; | |
} else { | |
// Handle JSON decoding errors | |
$output = 'Error: Invalid JSON provided - ' . json_last_error_msg(); | |
} | |
} | |
// ------------------------------------------------------------------------------------- | |
// HTML Output (Displays form and results) | |
// ------------------------------------------------------------------------------------- | |
?> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"/> | |
<title>Swag It (PHP 8 Attributes)</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIBWINXa8x5w==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | |
<style> | |
/* Style for the output block */ | |
pre { | |
cursor: copy; | |
border: 1px solid #e1e1e1; | |
padding: 15px; | |
background-color: #f9f9f9; | |
white-space: pre-wrap; /* Allow wrapping */ | |
word-wrap: break-word; /* Break long words/lines */ | |
font-size: 0.9em; | |
margin-top: 1rem; | |
} | |
code { | |
font-family: monospace; | |
} | |
/* Simple feedback for copy action */ | |
.copied-feedback { | |
display: inline-block; | |
margin-left: 10px; | |
padding: 2px 5px; | |
background-color: #d4edda; | |
color: #155724; | |
border-radius: 3px; | |
font-size: 0.8em; | |
opacity: 0; | |
transition: opacity 0.5s ease-out; | |
} | |
.copied-feedback.show { | |
opacity: 1; | |
} | |
/* Improved form styling */ | |
label { | |
font-weight: 600; | |
margin-top: 1rem; | |
} | |
textarea { | |
min-height: 200px; /* Increased min-height */ | |
} | |
.button-primary { | |
margin-top: 1rem; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Swag It (PHP 8 Attributes)</h1> | |
<p>A tool to help generate <a href="https://github.com/zircote/swagger-php">Swagger-PHP</a> <strong>PHP 8 Attribute</strong> syntax from an existing JSON payload. Inspired by <a href="https://gist.github.com/ls-dac-chartrand/5481b91bc97b828b348df8077269ff47">original code</a>.</p> | |
<form name="form1" method="post" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); // Use htmlspecialchars for security ?>"> | |
<div class="row"> | |
<div class="six columns"> | |
<label for="tabSize">Tab Width (for generated code)</label> | |
<input class="u-full-width" type="number" min=1 max=255 id="tabSize" name="tabSize" value="<?php echo $tabSize; ?>"> | |
</div> | |
<div class="six columns"> | |
<label for="tabInit">Initial Indent (base indent)</label> | |
<input class="u-full-width" type="number" min=0 max=255 id="tabInit" name="tabInit" value="<?php echo $tabInit; ?>"> | |
</div> | |
</div> | |
<label for="json">JSON Payload</label> | |
<textarea class="u-full-width" style="font-family: monospace;" id="json" name="json" | |
placeholder="Paste your JSON object or array here..."><?php echo htmlspecialchars($input); // Use htmlspecialchars for security ?></textarea><br> | |
<input class="button-primary" type="submit" value="Generate PHP Attributes"> | |
</form> | |
<?php | |
// Use htmlspecialchars for outputting code into HTML to prevent XSS | |
// and ensure correct display of <, >, & characters within the code. | |
if (!empty(trim($output))) { | |
// Output only the generated code | |
echo '<h4>Generated Code:<span class="copied-feedback" id="copyFeedback">Copied!</span></h4>'; | |
echo '<pre ondblclick="copySwagger(this);"><code id="swaggerOutput">' . htmlspecialchars(trim($output), ENT_QUOTES, 'UTF-8') . '</code></pre>'; | |
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && empty(trim($output)) && json_last_error() === JSON_ERROR_NONE) { | |
// Handle cases where conversion resulted in empty string (e.g., empty JSON) | |
echo '<p><em>Could not generate code. Check input JSON.</em></p>'; | |
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && json_last_error() !== JSON_ERROR_NONE) { | |
// Display JSON error if one occurred | |
echo '<p style="color: red;"><em>' . htmlspecialchars($output) . '</em></p>'; | |
} | |
?> | |
</div> | |
<script> | |
function copySwagger(element) { | |
const codeElement = element.querySelector('code'); // Get the <code> element | |
if (!codeElement) return; | |
const copyText = codeElement.textContent; | |
const textArea = document.createElement("textarea"); | |
// Basic styling to make it less intrusive | |
textArea.style.position = 'fixed'; | |
textArea.style.top = '-9999px'; // Move off-screen | |
textArea.style.left = '-9999px'; | |
textArea.value = copyText; // Use value for textarea | |
document.body.appendChild(textArea); | |
textArea.focus(); | |
textArea.select(); | |
let successful = false; | |
try { | |
successful = document.execCommand('copy'); | |
// console.log('Copying text command was ' + (successful ? 'successful' : 'unsuccessful')); | |
} catch (err) { | |
console.error('Oops, unable to copy', err); | |
alert('Oops, unable to copy to clipboard.'); // Fallback message | |
} | |
document.body.removeChild(textArea); | |
// Show feedback message | |
const feedback = document.getElementById('copyFeedback'); | |
if (feedback && successful) { | |
feedback.classList.add('show'); | |
setTimeout(() => { | |
feedback.classList.remove('show'); | |
}, 1500); // Hide after 1.5 seconds | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment