Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs): add github embeds #1037

Merged
merged 1 commit into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions docs/docs/guides/tutorials/php.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ sidebar_label: Symfony
keywords: [php, symfony]
---

import EmbedGitHubFileContent from "@site/src/components/EmbedGitHubFileContent.tsx";

# Using Hanko with the Symfony Framework

In this guide we are going to explain how to use Hanko with the Symfony framework for PHP. As Symfony is a full-stack framework with many abstractions for authentication management already present, we try to integrate Hanko as seamlessly as possible.
Expand All @@ -16,26 +18,26 @@ In this guide we are going to explain how to use Hanko with the Symfony framewor
## Creating and running the Symfony application
Use the following command to create a new symfony application from the Symfony demo template. `<demo-app-name>` is a placeholder for the name of the application (and directory in which it will be located). You can freely choose a `<demo-app-name>` that suits your needs and even describes your application best.

```
```bash
symfony new --demo <demo-app-name>
```
```

All following commands need to be run in the project directory so we move to this directory:

```
```bash
cd <demo-app-name>
```

To be able to work on the frontend parts of the project, we need to install all of its JavaScript dependencies first.
As usual we use NPM for this job.

```
```bash
npm install
```

We can now start the Symfony development server integrated in the Symfony CLI which serves your application on a local port.

```
```bash
symfony serve
```

Expand All @@ -44,7 +46,7 @@ You can now access your demo application using the link in the commands output.
## Integrating Hanko Frontend Components
To integrate the frontend components, we need to install the `@teamhanko/hanko-elements` package using NPM.

```
```bash
npm install @teamhanko/hanko-elements --save-dev
```

Expand All @@ -60,7 +62,9 @@ The placeholder `<id>` would be your Hanko cloud instance ID. If you don't use H

As we need to access the value of our new environment variable `HANKO_API_URL` somehow inside Twig templates, we chose to create a Twig-Extension:

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fsrc%2FTwig%2FHankoExtension.php&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/src/Twig/HankoExtension.php"
/>

As you can see, there is a `string $hankoApiUrl` parameter in the constructor function of this class. As Symfony auto-discovers TwigExtensions and tags them correctly, our class is going to be loaded and injected into the Twig environment right away.
Without "telling" the Symfony DI Container about the value for the `$hankoApiUrl` parameter, Symfony won't be able to instantiate our class. For service creation to work, we need to manually configure a service argument in `config/services.yaml `.
Expand All @@ -73,19 +77,23 @@ App\Twig\HankoExtension:

As the Symfony Demo Application uses Stimulus controllers with the Symfony UX stimulus-bridge for the original authentication forms, we adapt the `assets/controllers/login-controller.js` to load the `hanko-auth` custom element.

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fassets%2Fcontrollers%2Flogin-controller.js&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/assets/controllers/login-controller.js"
/>

As you can see, the adapted `login-controller` defines the stimulus values `hankoApiUrl` and `loginPath`.

Those values are provided in the `templates/security/login.html.twig` using the `stimulus_controller` Twig function.

There is also a stimulus target defined in the component and marked by the `stimulus_target` Twig helper function.

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Ftemplates%2Fsecurity%2Flogin.html.twig&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/templates/security/login.html.twig"
/>

The most important part of this template is the following:

```html
```twig
<div class="row" {{ stimulus_controller('login', {
'hankoApiUrl': hanko_api_url(),
'loginPath': path('security_login')
Expand All @@ -107,7 +115,9 @@ Leveraging the power of the Symfony Security component, we can authenticate the

The custom Authenticator for this example looks like this:

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fsrc%2FSecurity%2FHankoLoginAuthenticator.php&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/src/Security/HankoLoginAuthenticator.php"
/>

And has a dependency on three Composer packages which you need to install like this:

Expand Down Expand Up @@ -185,17 +195,23 @@ As the `database_users` provider cannot provide a user when the user registers f

The `hanko_users` provider has a custom service called `HankoUserProvider` associated to it, looking like this:

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fsrc%2FSecurity%2FHankoUserProvider.php&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/src/Security/HankoUserProvider.php"
/>

It creates a new `HankoUser` object using the given `$identifier` previously set from the JWTs `sub` claim in the `HankoUserProvider`.

When those steps are done, there is either a `HankoUser` or a normal `User` object set in the Symfony Security module. Depending on which type of User is currently authenticated, we can decide to just show a registration form and don't allow the user to go further using a custom `entry_point` in the `main` firewall part of the `security.yaml` configuration.

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fsrc%2FSecurity%2FHankoAuthenticationEntryPoint.php&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/src/Security/HankoAuthenticationEntryPoint.php"
/>

Additionally we need to create a new `EventSubscriber` listening on all `KernelEvents::REQUEST` events to redirect users from every other URL than the registration URL back there.

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fsrc%2FEventSubscriber%2FUpgradeHankoUserSubscriber.php&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/src/EventSubscriber/UpgradeHankoUserSubscriber.php"
/>

For the purpose of registering a new user, a new Controller method called `register` placed in the `SecurityController` of the Demo project is required looking like this:

Expand Down Expand Up @@ -294,7 +310,9 @@ export default class extends Controller {

The Stimulus targets used by the controller displayed above aren't set using the `stimulus_`-Twig helper functions but provided in the `UserType` Form-Type.

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fsrc%2FForm%2FUserType.php&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/src/Form/UserType.php"
/>

## Modifying the User entity and removing passwords from the application

Expand All @@ -319,4 +337,6 @@ We also need to do some manual steps to allow users to log out of their account

For this, another `EventSUbscriber` is required:

`<script src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fteamhanko%2Fsymfony-example%2Fblob%2Fmain%2Fsrc%2FEventSubscriber%2FLogoutHankoUserSubscriber.php&style=default&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on&fetchFromJsDelivr=on"></script>`
<EmbedGitHubFileContent
url="https://github.com/teamhanko/symfony-example/blob/main/src/EventSubscriber/LogoutHankoUserSubscriber.php"
/>
1 change: 1 addition & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const config = {
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
additionalLanguages: ['php', 'bash']
},
}),
};
Expand Down
96 changes: 96 additions & 0 deletions docs/src/components/EmbedGitHubFileContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { FC } from "react";
import CodeBlock from '@theme/CodeBlock';

function convertGitHubUrlToRaw(url: string): string {
const [user, repo, blob, branch, ...filePath] = url.replace("https://github.com/", "").split("/");
const rawFileUrl = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${filePath.join("/")}`;
return rawFileUrl;
}

function getFileExtension(url: string): string {
const filename = url.split("/").pop();
return filename.slice((Math.max(0, filename.lastIndexOf(".")) || Infinity) + 1);
}

function getFilePath(url: string): string {
return url.replace("https://github.com/", "").split("/").slice(4, Infinity).join("/");
}

type EmbedGitHubFileContentProps = {
url: string,
loadingComponent?: JSX.Element,
errorComponent?: JSX.Element,
onLoad?: () => void
onError?: (e: Error) => void
}

const EmbedGitHubFileContent: FC<EmbedGitHubFileContentProps> = ({
url,
loadingComponent,
errorComponent,
onLoad,
onError
}) => {
const [gitHubFileContent, setGitHubFileContent] = React.useState("");
const [isLoading, setIsLoading] = React.useState(true);
const [errorOccurred, setErrorOccurred] = React.useState(false);

const gitHubUrlRegEx = /^https:\/\/github\.com\/.*\/.*\/blob\/.*/;
if (!url.match(gitHubUrlRegEx)) {
throw new Error("Invalid URL format");
}

React.useEffect(() => {
setGitHubFileContent("");
setIsLoading(true);
setErrorOccurred(false);

const rawFileUrl = convertGitHubUrlToRaw(url);

const fetchGitHubFileContent = async () => {
try {
const response = await fetch(rawFileUrl, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});

if (!response.ok) {
setErrorOccurred(true);
onError(new Error());
} else {
const text = await response.text();
setGitHubFileContent(text);
setIsLoading(false);
onLoad();
}
} catch (err) {
setErrorOccurred(true);
onError(err);
}
};

fetchGitHubFileContent();
}, [url, onLoad, onError]);

if (errorOccurred) {
return errorComponent;
}

if (isLoading) {
return loadingComponent;
}

return (
<CodeBlock language={getFileExtension(url)} title={getFilePath(url)}>{gitHubFileContent}</CodeBlock>
);
}

EmbedGitHubFileContent.defaultProps = {
loadingComponent: <p>loading...</p>,
errorComponent: <p>an error occured.</p>,
onLoad: () => {},
onError: (e) => console.log(e)
};

export default EmbedGitHubFileContent;
Loading