{"id":25623192,"url":"https://github.com/cosmoverse/awaitform","last_synced_at":"2025-09-05T03:41:24.674Z","repository":{"id":278884998,"uuid":"937076561","full_name":"Cosmoverse/AwaitForm","owner":"Cosmoverse","description":"Write clean form navigation flows in PocketMine-MP using async/await pattern.","archived":false,"fork":false,"pushed_at":"2025-07-24T05:40:08.000Z","size":38,"stargazers_count":14,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-30T12:31:24.278Z","etag":null,"topics":["async","forms","library","php","pmmp","pocketmine-mp"],"latest_commit_sha":null,"homepage":"https://poggit.pmmp.io/ci/Cosmoverse/AwaitForm","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Cosmoverse.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-02-22T09:22:31.000Z","updated_at":"2025-07-24T05:37:42.000Z","dependencies_parsed_at":"2025-02-22T10:26:05.732Z","dependency_job_id":"7550ad21-fa12-4f52-aab3-490612ce6f11","html_url":"https://github.com/Cosmoverse/AwaitForm","commit_stats":null,"previous_names":["cosmoverse/awaitform"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/Cosmoverse/AwaitForm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cosmoverse%2FAwaitForm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cosmoverse%2FAwaitForm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cosmoverse%2FAwaitForm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cosmoverse%2FAwaitForm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Cosmoverse","download_url":"https://codeload.github.com/Cosmoverse/AwaitForm/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cosmoverse%2FAwaitForm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273708967,"owners_count":25153728,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-05T02:00:09.113Z","response_time":402,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","forms","library","php","pmmp","pocketmine-mp"],"created_at":"2025-02-22T11:23:17.595Z","updated_at":"2025-09-05T03:41:24.648Z","avatar_url":"https://github.com/Cosmoverse.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AwaitForm\nWrite clean form navigation flows in PocketMine-MP using async/await pattern in PHP.\n\n## Motive\nForm navigation flows are inherently asynchronous. Existing libraries use callbacks or specialized Form classes to\nhandle responses. Control flow syntax (e.g., while, for, continue, break) cannot be fully utilized as each form handler\ngets its own isolated context. This makes several tasks challenging.\n\nNavigation flow becomes incomprehensible in code when Form A sends user to Form B before bringing them back to Form A,\nbut this time with different parameters for Form A. This is often encountered with pagination ('Previous Page' and 'Next\nPage' buttons), refresh mechanisms (e.g., a 'Refresh' button), and non-dismissible forms (i.e., disallowing users to\nback away).\n\nOther issues left unaddressed by conventional APIs is no way to detect and handle failure when sending forms, no defined\ncleanup/finalization routine for navigation flows, and no shared state for nested forms (Form A→B→A). Existing libraries\nhave incorporated specialized paginated forms to avoid boilerplate, and explicit mechanisms in nested forms to allow\nnavigating back from child to parent form.\n\n## Approach\nAwaitForm addresses existing issues through an alternative async/await based form-handling syntax using\n[await-generator](https://github.com/SOF3/await-generator).\n```php\n$form = AwaitForm::form(\"Create a Ban Report\", [\n\tFormControl::input(\"Player\", \"Enter their gamertag\"),\n\tFormControl::dropdown(\"Ban Reason\", [\"Hacking\", \"Spamming\", \"Toxicity\"]),\n\tFormControl::input(\"Comment\", \"Any further comments...\")\n]);\n[$gamertag, $reason, $comment] = yield from $form-\u003erequest($player);\n```\n\u003cdetails align=\"center\"\u003e\n\t\u003csummary\u003eSee demo\u003c/summary\u003e\n\n![Ban report form](https://github.com/user-attachments/assets/568c3775-6a2e-42d4-9383-ef559093d750)\n\u003c/details\u003e\n\nUsers get to utilize native PHP control flow syntax (while, for, continue, break, etc.; see\n[Retry Logic in Form](#1-retry-logic-in-form)) instead of a costly reimplementation of existing control structures which\nexisting libraries achieve using callbacks. AwaitForm features no additional special-purpose mechanism, but still aids\nusers in making otherwise complex [paginated](#3-paginated-button-menu) and [nested](#4-nested-forms) navigation flows.\n```php\n// -- initialization: e.g., make player immobile when viewing form --\n$player-\u003esetNoClientPredictions(true);\nwhile(true){\n\t$form = AwaitForm::form(\"Set home here?\", [FormControl::input(\"Home Name:\")]);\n\t// -- request: send form and wait for response --\n\ttry{\n\t\t[$name] = yield from $form-\u003erequest($player);\n\t}catch(AwaitFormException){\n\t\t// -- failure: exit loop if player closes form or disconnects --\n\t\tbreak;\n\t}\n\t// -- evaluate: handle response --\n\tif(trim($name) === \"\"){\n\t\t$player-\u003esendToastNotification(\"Invalid Name\", \"Home name cannot be empty\");\n\t\tcontinue;\n\t}\n\t$player-\u003esendMessage(\"Home '{$name}' set at your location!\");\n\tbreak;\n}\n// -- finalization/cleanup: e.g., revert player movement restriction --\n$player-\u003esetNoClientPredictions(false);\n```\n\u003cdetails align=\"center\"\u003e\n\t\u003csummary\u003eSee demo\u003c/summary\u003e\n\t\nhttps://github.com/user-attachments/assets/ef36329b-a7e9-4d83-bdfe-b7920e3da5d3\n\u003c/details\u003e\n\n### When the user does not respond\nPlayer disconnects, server shutdowns, validation errors, and 'busy status' throw an `AwaitFormException`. Read\n`AwaitFormException::getCode()` to narrow down the cause to `ERR_VALIDATION_FAILED`, `ERR_PLAYER_REJECTED`, or\n`ERR_PLAYER_QUIT`.\n```php\ntry{\n\t$response = yield from $form-\u003erequest($player);\n}catch(AwaitFormException){\n\treturn;\n}\n$player-\u003esendMessage(\"Response: \" . json_encode($response));\n$player-\u003esendMessage(\"Report Received, thank you!\");\n```\n\n## Example Design Models\n### 1. Retry logic in form\nRevisiting the example above (creating a ban report), player gamertags require validation. In this example, the player\nis sent the form again when they enter a wrong gamertag. This design includes State Persistence whereby the user's input\nis not lost upon entering a wrong gamertag.\n```php\n$gamertag = \"\";\n$reason = null;\n$comment = \"\";\nwhile(true){\n\t$form = AwaitForm::form(\"Create a Ban Report\", [\n\t\tFormControl::input(\"Player\", \"Enter their gamertag\", $gamertag),\n\t\tFormControl::dropdown(\"Ban Reason\", [\"Hacking\", \"Spamming\", \"Toxicity\"], $reason),\n\t\tFormControl::input(\"Comment\", \"Any further comments...\", $comment)\n\t]);\n\ttry{\n\t\t[$gamertag, $reason, $comment] = yield from $form-\u003erequest($player);\n\t}catch(AwaitFormException){\n\t\tbreak;\n\t}\n\tif(!$server-\u003ehasOfflinePlayerData($gamertag)){\n\t\t$player-\u003esendToastNotification(\"Player Not Found\", \"'{$gamertag}' never joined this server.\");\n\t\tcontinue;\n\t}\n\t$player-\u003esendMessage(\"Response: \" . json_encode([$gamertag, $reason, $comment]));\n\t$player-\u003esendMessage(\"Report Received, thank you!\");\n\tbreak;\n}\n```\n\u003cdetails align=\"center\"\u003e\n\t\u003csummary\u003eSee demo\u003c/summary\u003e\t\n\nhttps://github.com/user-attachments/assets/e75081f5-66c3-44cb-bdcd-fb9ff5b7f1e5\n\u003c/details\u003e\n\n### 2. Non-dismissible form\nA player is banned and is forced to acknowledge their ban. If they close the form, the form is sent again -\nthey cannot back away. They are also given permanent blindness until then.\n```php\n// -- initialization: happens before main loop --\n$player-\u003egetEffects()-\u003eadd(new EffectInstance(VanillaEffects::BLINDNESS(), Limits::INT32_MAX));\nwhile(true){\n\t$form = AwaitForm::form(\"You are BANNED!\", [\n\t\tFormControl::toggle(\"I acknowledge my ban.\"),\n\t\tFormControl::input(\"Comments\", \"Type any comments you have...\")\n\t]);\n\ttry{\n\t\t[$acknowledged, $comments] = yield from $form-\u003erequest($player);\n\t}catch(AwaitFormException $e){\n\t\tif($e-\u003egetCode() === AwaitFormException::ERR_PLAYER_QUIT){\n\t\t\tbreak;\n\t\t}\n\t\tcontinue;\n\t}\n\tif($acknowledged){\n\t\techo \"Comments: \", $comments, PHP_EOL;\n\t\tbreak;\n\t}\n\t$player-\u003esendToastNotification(\"Try Again\", \"Acknowledgement is needed.\");\n}\n$player-\u003egetEffects()-\u003eremove(VanillaEffects::BLINDNESS());\n```\n\u003cdetails align=\"center\"\u003e\n\t\u003csummary\u003eSee demo\u003c/summary\u003e\n\nhttps://github.com/user-attachments/assets/04657987-23ec-4649-96fc-a86f5fe1811e\n\u003c/details\u003e\n\n### 3. Paginated button menu\nPlayers can spawn combat items on a PvP server. 10 items are listed at a time in a menu.\nFor pagination, there is a 'Previous Page' and a 'Next Page' button at the very end of\nthe menu.\n```php\n// -- initialization: shared state variables used across all pages --\n$items = array_filter(VanillaItems::getAll(), fn($item) =\u003e $item instanceof Durable);\n$offset = 0;\n$length = 10;\nwhile(true){\n\t$sublist = array_slice($items, $offset, $length);\n\t$buttons = [];\n\tforeach($sublist as $id =\u003e $item){\n\t\t$buttons[$id] = Button::simple($item-\u003egetName());\n\t}\n\tif($offset \u003e 0) $buttons[\"prev\"] = Button::simple(\"[Previous Page]\");\n\tif($offset + $length \u003c count($items)) $buttons[\"next\"] = Button::simple(\"[Next Page]\");\n\t$form = AwaitForm::menu(\"Free Items!\", \"Have fun soldier :)\", $buttons);\n\ttry{\n\t\t$response = yield from $form-\u003erequest($player);\n\t}catch(AwaitFormException){\n\t\tbreak;\n\t}\n\tif($response === \"prev\"){\n\t\t$offset -= $length; // validation by-design: can never go negative\n\t}elseif($response === \"next\"){\n\t\t$offset += $length;\n\t}else{\n\t\t$item = $sublist[$response];\n\t\t$player-\u003egetInventory()-\u003eaddItem($item);\n\t}\n}\n```\n\u003cdetails align=\"center\"\u003e\n\t\u003csummary\u003eSee demo\u003c/summary\u003e\n\nhttps://github.com/user-attachments/assets/82296361-4741-46f7-b227-a1e577a91083\n\u003c/details\u003e\n\n### 4. Nested forms\nRevisiting the first example (creating a ban report), this change adds a confirmation form and a mechanism to store\nreports using a Finite State Machine.\n\nFinite State Machines in modeling user interfaces allow you to think at a higher level of abstraction. Instead of\nthinking _\"After player fills a ban report; the gamertag and the reason is displayed with a yes/no button to confirm\nfiling the report\"_, you think _\"The UI is put in a CONFIRM state upon filing the report\"_ and entering the state means\ncertain things happen.\n\n```php\n$gamertag = \"\";\n$reason = null;\n$comment = \"\";\n$state = \"CREATE\";\nwhile($state !== \"DESTROY\"){\n\tif($state === \"CREATE\"){\n\t\t$form = AwaitForm::form(\"Create a Ban Report\", [\n\t\t\tFormControl::input(\"Player\", \"Enter their gamertag\", $gamertag),\n\t\t\tFormControl::dropdown(\"Ban Reason\", [\"Hacking\", \"Spamming\", \"Toxicity\"], $reason),\n\t\t\tFormControl::input(\"Comment\", \"Any further comments...\", $comment)\n\t\t]);\n\t\ttry{\n\t\t\t[$gamertag, $reason, $comment] = yield from $form-\u003erequest($player);\n\t\t}catch(AwaitFormException){\n\t\t\t$state = \"DESTROY\";\n\t\t\tcontinue;\n\t\t}\n\t\tif(!$server-\u003ehasOfflinePlayerData($gamertag)){\n\t\t\t$player-\u003esendToastNotification(\"Player Not Found\", \"'{$gamertag}' never joined this server.\");\n\t\t\tcontinue;\n\t\t}\n\t\t$state = \"CONFIRM\";\n\t}elseif($state === \"CONFIRM\"){\n\t\t$message = [\"Are you sure you would like to file this report? Review your details:\"];\n\t\t$message[] = \"Gamertag: {$gamertag}\";\n\t\t$message[] = \"Reason: {$reason}\";\n\t\t$message[] = \"Comment: {$comment}\";\n\t\t$form = AwaitForm::menu(\"Confirm Filing Report?\", implode(TextFormat::EOL, $message), [\n\t\t\t\"yes\" =\u003e Button::simple(\"Confirm\"),\n\t\t\t\"edit\" =\u003e Button::simple(\"Make Changes\"),\n\t\t\t\"no\" =\u003e Button::simple(\"Cancel\")\n\t\t]);\n\t\t$response = yield from $form-\u003erequestOrFallback($player, \"no\");\n\t\t$state = match($response){\n\t\t\t\"yes\" =\u003e \"WRITE\",\n\t\t\t\"edit\" =\u003e \"CREATE\",\n\t\t\t\"no\" =\u003e \"DESTROY\"\n\t\t};\n\t}elseif($state === \"WRITE\"){\n\t\tyield from $database-\u003easyncInsert(\"myplugin.ban_reports\", [\"offender\" =\u003e $gamertag, \"reason\" =\u003e $reason, \"comment\" =\u003e $comment]);\n\t\tif($player-\u003eisConnected()){\n\t\t\t$player-\u003esendToastNotification(\"Report Successful!\", \"Thank you very much.\");\n\t\t}\n\t\t$gamertag = \"\";\n\t\t$reason = null;\n\t\t$comment = \"\";\n\t\t$state = \"CREATE\";\n\t}\n}\n```\n\u003cdetails align=\"center\"\u003e\n\t\u003csummary\u003eSee demo\u003c/summary\u003e\t\n\nhttps://github.com/user-attachments/assets/e654822f-598a-4b67-af93-049de33197a2\n\u003c/details\u003e\n\n## Reusing Forms\nForm windows store only display properties and not player state. A form window (i.e., `AwaitForm::dialog()`,\n`AwaitForm::form()`, `AwaitForm::menu()`) may be instantiated once and reused multiple times. All window properties that\nare not readonly are allowed to be mutated.\n```php\n$form = AwaitForm::menu(\"title\", \"content\", []);\n$form-\u003etitle = \"New title\";\n$form-\u003ebuttons[] = [Button::simple(\"Get free food\"), \"food\"];\n$form-\u003ebuttons[] = [Button::simple(\"Get free block\"), \"block\"];\nwhile(true){\n\ttry{\n\t\tyield from $form-\u003erequest($player);\n\t}catch(AwaitFormException){\n\t\tbreak;\n\t}\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcosmoverse%2Fawaitform","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcosmoverse%2Fawaitform","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcosmoverse%2Fawaitform/lists"}