token=$token;return $this;}
public function request(string $method,string $path,?array $body=null): mixed {
if(!str_starts_with($path,'/api/v1/'))throw new \InvalidArgumentException('Use an /api/v1/ path.');if(!extension_loaded('curl'))throw new \RuntimeException('PHP cURL is required.');
$headers=['Accept: application/json'];if($this->token!=='')$headers[]='Authorization: Bearer '.$this->token;$options=[CURLOPT_CUSTOMREQUEST=>$method,CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>false,CURLOPT_TIMEOUT=>180,CURLOPT_CONNECTTIMEOUT=>5];if($body!==null){$headers[]='Content-Type: application/json';$options[CURLOPT_POSTFIELDS]=json_encode((object)$this->normalize($body),JSON_THROW_ON_ERROR);}$options[CURLOPT_HTTPHEADER]=$headers;$c=curl_init(rtrim($this->base,'/').$path);curl_setopt_array($c,$options);$raw=curl_exec($c);$status=curl_getinfo($c,CURLINFO_RESPONSE_CODE);curl_close($c);if($raw===false)throw new ApiException(503,'','Could not connect to API.');$r=json_decode($raw,true,64,JSON_THROW_ON_ERROR);if(!($r['ok']??false))throw new ApiException($status,$r['meta']['requestId']??'',$r['error']['message']??'API error');return $r['data'];
}
private function normalize(array $data): array {foreach($data as $key=>&$value){if(is_array($value))$value=$this->normalize($value);}unset($value);if(isset($data['id'],$data['type'],$data['version'],$data['props'],$data['children']))$data['props']=(object)$data['props'];return $data;}
public function login(string $email,string $password): array{$r=$this->request('POST','/api/v1/auth/login',compact('email','password'));$this->token($r['token']);return $r;}
public function component(string $type,string $id,array $props=[],array $children=[]): RemoteComponent{return new RemoteComponent($this,$type,$id,$props,$children);}
public function page(array $document,string $slug): RemotePage{return new RemotePage($this,$document,$slug);}
public function publishedPage(string $site,string $slug): array{return $this->request('GET','/api/v1/public/sites/'.rawurlencode($site).'/pages/'.rawurlencode($slug).'/render');}
}
class RemoteComponent implements \JsonSerializable {
public function __construct(protected Client $client,protected string $type,protected string $id,protected array $props=[],protected array $children=[]){}
public function prop(string $name,mixed $value): static{$this->props[$name]=$value;return $this;}
public function add(RemoteComponent $child): static{$this->children[]=$child;return $this;}
public function jsonSerialize(): array{return ['id'=>$this->id,'type'=>$this->type,'version'=>'1.0.0','props'=>(object)$this->props,'children'=>array_map(fn($child)=>$child instanceof self?$child->jsonSerialize():$child,$this->children)];}
public function render(): array{return $this->client->request('POST','/api/v1/components/render',['component'=>$this->jsonSerialize()]);}
public function html(): string{$r=$this->render();return ''.$r['html'].'';}
public function print(): void{echo $this->html();}
}
class RemotePage {
public function __construct(protected Client $client,protected array $document,protected string $slug){}
public function add(RemoteComponent $component): static {foreach($this->document['pages'] as &$page)if($page['slug']===$this->slug){$page['children'][]=$component->jsonSerialize();return $this;}throw new \InvalidArgumentException('Page not found.');}
public function render(): array{return $this->client->request('POST','/api/v1/pages/render',['document'=>$this->document,'page'=>$this->slug]);}
public function print(): void{echo $this->render()['html'];}
}