Skip to content

Instantly share code, notes, and snippets.

@alt-st
Forked from ls-dac-chartrand/swagit.php
Created April 17, 2025 12:27
Show Gist options
  • Save alt-st/c7ce345e7b5302de169e60b841e2e6fa to your computer and use it in GitHub Desktop.
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
<?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