From b8a89d86a03af4fbf4c771aa19230d24ccabc76b Mon Sep 17 00:00:00 2001
From: zhengkunwang <31820853+zhengkunwang223@users.noreply.github.com>
Date: Tue, 2 Jan 2024 21:54:28 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20PHP=20=E8=BF=90=E8=A1=8C=E7=8E=AF?=
 =?UTF-8?q?=E5=A2=83=E5=A2=9E=E5=8A=A0=E6=89=A9=E5=B1=95=E6=A8=A1=E7=89=88?=
 =?UTF-8?q?=20(#3502)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Refs https://github.com/1Panel-dev/1Panel/issues/1636
---
 backend/app/api/v1/entry.go                   |  13 +-
 backend/app/api/v1/php_extensions.go          | 103 +++++++++
 backend/app/dto/request/php_extensions.go     |  22 ++
 backend/app/dto/response/php_extensions.go    |   7 +
 backend/app/model/php_extensions.go           |   7 +
 backend/app/repo/php_extensions.go            |  59 +++++
 backend/app/service/entry.go                  |   9 +-
 backend/app/service/php_extensions.go         |  86 +++++++
 backend/init/migration/migrate.go             |   2 +
 backend/init/migration/migrations/v_1_9.go    |  13 ++
 backend/router/ro_runtime.go                  |   6 +
 cmd/server/docs/docs.go                       | 217 ++++++++++++++++++
 cmd/server/docs/swagger.json                  | 217 ++++++++++++++++++
 cmd/server/docs/swagger.yaml                  | 136 +++++++++++
 frontend/src/api/interface/runtime.ts         |  25 ++
 frontend/src/api/modules/runtime.ts           |  22 +-
 frontend/src/global/form-rules.ts             |  19 ++
 frontend/src/lang/modules/en.ts               |   5 +
 frontend/src/lang/modules/tw.ts               |   5 +
 frontend/src/lang/modules/zh.ts               |   5 +
 .../website/runtime/php/create/index.vue      |  32 ++-
 .../website/runtime/php/extensions/index.vue  | 101 ++++++++
 .../runtime/php/extensions/operate/index.vue  | 121 ++++++++++
 .../src/views/website/runtime/php/index.vue   |  13 +-
 24 files changed, 1231 insertions(+), 14 deletions(-)
 create mode 100644 backend/app/api/v1/php_extensions.go
 create mode 100644 backend/app/dto/request/php_extensions.go
 create mode 100644 backend/app/dto/response/php_extensions.go
 create mode 100644 backend/app/model/php_extensions.go
 create mode 100644 backend/app/repo/php_extensions.go
 create mode 100644 backend/app/service/php_extensions.go
 create mode 100644 frontend/src/views/website/runtime/php/extensions/index.vue
 create mode 100644 frontend/src/views/website/runtime/php/extensions/operate/index.vue

diff --git a/backend/app/api/v1/entry.go b/backend/app/api/v1/entry.go
index 058dd497b..f1638c09f 100644
--- a/backend/app/api/v1/entry.go
+++ b/backend/app/api/v1/entry.go
@@ -21,10 +21,10 @@ var (
 	imageService           = service.NewIImageService()
 	dockerService          = service.NewIDockerService()
 
-	mysqlService    = service.NewIMysqlService()
-	postgresqlService    = service.NewIPostgresqlService()
-	databaseService = service.NewIDatabaseService()
-	redisService    = service.NewIRedisService()
+	mysqlService      = service.NewIMysqlService()
+	postgresqlService = service.NewIPostgresqlService()
+	databaseService   = service.NewIDatabaseService()
+	redisService      = service.NewIRedisService()
 
 	cronjobService = service.NewICronjobService()
 
@@ -53,8 +53,9 @@ var (
 	snapshotService = service.NewISnapshotService()
 	upgradeService  = service.NewIUpgradeService()
 
-	runtimeService = service.NewRuntimeService()
-	processService = service.NewIProcessService()
+	runtimeService       = service.NewRuntimeService()
+	processService       = service.NewIProcessService()
+	phpExtensionsService = service.NewIPHPExtensionsService()
 
 	hostToolService = service.NewIHostToolService()
 
diff --git a/backend/app/api/v1/php_extensions.go b/backend/app/api/v1/php_extensions.go
new file mode 100644
index 000000000..9af31f4f1
--- /dev/null
+++ b/backend/app/api/v1/php_extensions.go
@@ -0,0 +1,103 @@
+package v1
+
+import (
+	"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
+	"github.com/1Panel-dev/1Panel/backend/app/dto"
+	"github.com/1Panel-dev/1Panel/backend/app/dto/request"
+	"github.com/1Panel-dev/1Panel/backend/constant"
+	"github.com/gin-gonic/gin"
+)
+
+// @Tags PHP Extensions
+// @Summary Page Extensions
+// @Description Page Extensions
+// @Accept json
+// @Param request body request.PHPExtensionsSearch true "request"
+// @Success 200 {array} response.PHPExtensionsDTO
+// @Security ApiKeyAuth
+// @Router /runtimes/php/extensions/search [post]
+func (b *BaseApi) PagePHPExtensions(c *gin.Context) {
+	var req request.PHPExtensionsSearch
+	if err := helper.CheckBindAndValidate(&req, c); err != nil {
+		return
+	}
+	if req.All {
+		list, err := phpExtensionsService.List()
+		if err != nil {
+			helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
+			return
+		}
+		helper.SuccessWithData(c, list)
+	} else {
+		total, list, err := phpExtensionsService.Page(req)
+		if err != nil {
+			helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
+			return
+		}
+		helper.SuccessWithData(c, dto.PageResult{
+			Total: total,
+			Items: list,
+		})
+	}
+
+}
+
+// @Tags PHP Extensions
+// @Summary Create Extensions
+// @Description Create Extensions
+// @Accept json
+// @Param request body request.PHPExtensionsCreate true "request"
+// @Success 200
+// @Security ApiKeyAuth
+// @Router /runtimes/php/extensions [post]
+func (b *BaseApi) CreatePHPExtensions(c *gin.Context) {
+	var req request.PHPExtensionsCreate
+	if err := helper.CheckBindAndValidate(&req, c); err != nil {
+		return
+	}
+	if err := phpExtensionsService.Create(req); err != nil {
+		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
+		return
+	}
+	helper.SuccessWithOutData(c)
+}
+
+// @Tags PHP Extensions
+// @Summary Update Extensions
+// @Description Update Extensions
+// @Accept json
+// @Param request body request.PHPExtensionsUpdate true "request"
+// @Success 200
+// @Security ApiKeyAuth
+// @Router /runtimes/php/extensions/update [post]
+func (b *BaseApi) UpdatePHPExtensions(c *gin.Context) {
+	var req request.PHPExtensionsUpdate
+	if err := helper.CheckBindAndValidate(&req, c); err != nil {
+		return
+	}
+	if err := phpExtensionsService.Update(req); err != nil {
+		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
+		return
+	}
+	helper.SuccessWithOutData(c)
+}
+
+// @Tags PHP Extensions
+// @Summary Delete Extensions
+// @Description Delete Extensions
+// @Accept json
+// @Param request body request.PHPExtensionsDelete true "request"
+// @Success 200
+// @Security ApiKeyAuth
+// @Router /runtimes/php/extensions/del [post]
+func (b *BaseApi) DeletePHPExtensions(c *gin.Context) {
+	var req request.PHPExtensionsDelete
+	if err := helper.CheckBindAndValidate(&req, c); err != nil {
+		return
+	}
+	if err := phpExtensionsService.Delete(req); err != nil {
+		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
+		return
+	}
+	helper.SuccessWithOutData(c)
+}
diff --git a/backend/app/dto/request/php_extensions.go b/backend/app/dto/request/php_extensions.go
new file mode 100644
index 000000000..a163859ff
--- /dev/null
+++ b/backend/app/dto/request/php_extensions.go
@@ -0,0 +1,22 @@
+package request
+
+import "github.com/1Panel-dev/1Panel/backend/app/dto"
+
+type PHPExtensionsSearch struct {
+	dto.PageInfo
+	All bool `json:"all"`
+}
+
+type PHPExtensionsCreate struct {
+	Name       string `json:"name" validate:"required"`
+	Extensions string `json:"extensions" validate:"required"`
+}
+
+type PHPExtensionsUpdate struct {
+	ID         uint   `json:"id" validate:"required"`
+	Extensions string `json:"extensions" validate:"required"`
+}
+
+type PHPExtensionsDelete struct {
+	ID uint `json:"id" validate:"required"`
+}
diff --git a/backend/app/dto/response/php_extensions.go b/backend/app/dto/response/php_extensions.go
new file mode 100644
index 000000000..91a96f0d2
--- /dev/null
+++ b/backend/app/dto/response/php_extensions.go
@@ -0,0 +1,7 @@
+package response
+
+import "github.com/1Panel-dev/1Panel/backend/app/model"
+
+type PHPExtensionsDTO struct {
+	model.PHPExtensions
+}
diff --git a/backend/app/model/php_extensions.go b/backend/app/model/php_extensions.go
new file mode 100644
index 000000000..0055bd258
--- /dev/null
+++ b/backend/app/model/php_extensions.go
@@ -0,0 +1,7 @@
+package model
+
+type PHPExtensions struct {
+	BaseModel
+	Name       string ` json:"name" gorm:"not null"`
+	Extensions string `json:"extensions" gorm:"not null"`
+}
diff --git a/backend/app/repo/php_extensions.go b/backend/app/repo/php_extensions.go
new file mode 100644
index 000000000..81da5f364
--- /dev/null
+++ b/backend/app/repo/php_extensions.go
@@ -0,0 +1,59 @@
+package repo
+
+import (
+	"github.com/1Panel-dev/1Panel/backend/app/model"
+)
+
+type PHPExtensionsRepo struct {
+}
+
+type IPHPExtensionsRepo interface {
+	Page(page, size int, opts ...DBOption) (int64, []model.PHPExtensions, error)
+	Save(extension *model.PHPExtensions) error
+	Create(extension *model.PHPExtensions) error
+	GetFirst(opts ...DBOption) (model.PHPExtensions, error)
+	DeleteBy(opts ...DBOption) error
+	List() ([]model.PHPExtensions, error)
+}
+
+func NewIPHPExtensionsRepo() IPHPExtensionsRepo {
+	return &PHPExtensionsRepo{}
+}
+
+func (p *PHPExtensionsRepo) Page(page, size int, opts ...DBOption) (int64, []model.PHPExtensions, error) {
+	var (
+		phpExtensions []model.PHPExtensions
+	)
+	db := getDb(opts...).Model(&model.PHPExtensions{})
+	count := int64(0)
+	db = db.Count(&count)
+	err := db.Limit(size).Offset(size * (page - 1)).Find(&phpExtensions).Error
+	return count, phpExtensions, err
+}
+
+func (p *PHPExtensionsRepo) List() ([]model.PHPExtensions, error) {
+	var (
+		phpExtensions []model.PHPExtensions
+	)
+	err := getDb().Model(&model.PHPExtensions{}).Find(&phpExtensions).Error
+	return phpExtensions, err
+}
+
+func (p *PHPExtensionsRepo) Save(extension *model.PHPExtensions) error {
+	return getDb().Save(&extension).Error
+}
+
+func (p *PHPExtensionsRepo) Create(extension *model.PHPExtensions) error {
+	return getDb().Create(&extension).Error
+}
+
+func (p *PHPExtensionsRepo) GetFirst(opts ...DBOption) (model.PHPExtensions, error) {
+	var extension model.PHPExtensions
+	db := getDb(opts...).Model(&model.PHPExtensions{})
+	err := db.First(&extension).Error
+	return extension, err
+}
+
+func (p *PHPExtensionsRepo) DeleteBy(opts ...DBOption) error {
+	return getDb(opts...).Delete(&model.PHPExtensions{}).Error
+}
diff --git a/backend/app/service/entry.go b/backend/app/service/entry.go
index 4b2956eb3..bb97578a3 100644
--- a/backend/app/service/entry.go
+++ b/backend/app/service/entry.go
@@ -12,9 +12,9 @@ var (
 	appInstallRepo         = repo.NewIAppInstallRepo()
 	appInstallResourceRepo = repo.NewIAppInstallResourceRpo()
 
-	mysqlRepo    = repo.NewIMysqlRepo()
-	postgresqlRepo    = repo.NewIPostgresqlRepo()
-	databaseRepo = repo.NewIDatabaseRepo()
+	mysqlRepo      = repo.NewIMysqlRepo()
+	postgresqlRepo = repo.NewIPostgresqlRepo()
+	databaseRepo   = repo.NewIDatabaseRepo()
 
 	imageRepoRepo = repo.NewIImageRepoRepo()
 	composeRepo   = repo.NewIComposeTemplateRepo()
@@ -38,7 +38,8 @@ var (
 	logRepo      = repo.NewILogRepo()
 	snapshotRepo = repo.NewISnapshotRepo()
 
-	runtimeRepo = repo.NewIRunTimeRepo()
+	runtimeRepo       = repo.NewIRunTimeRepo()
+	phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
 
 	favoriteRepo = repo.NewIFavoriteRepo()
 )
diff --git a/backend/app/service/php_extensions.go b/backend/app/service/php_extensions.go
new file mode 100644
index 000000000..665d71857
--- /dev/null
+++ b/backend/app/service/php_extensions.go
@@ -0,0 +1,86 @@
+package service
+
+import (
+	"github.com/1Panel-dev/1Panel/backend/app/dto/request"
+	"github.com/1Panel-dev/1Panel/backend/app/dto/response"
+	"github.com/1Panel-dev/1Panel/backend/app/model"
+	"github.com/1Panel-dev/1Panel/backend/buserr"
+	"github.com/1Panel-dev/1Panel/backend/constant"
+)
+
+type PHPExtensionsService struct {
+}
+
+type IPHPExtensionsService interface {
+	Page(req request.PHPExtensionsSearch) (int64, []response.PHPExtensionsDTO, error)
+	List() ([]response.PHPExtensionsDTO, error)
+	Create(req request.PHPExtensionsCreate) error
+	Update(req request.PHPExtensionsUpdate) error
+	Delete(req request.PHPExtensionsDelete) error
+}
+
+func NewIPHPExtensionsService() IPHPExtensionsService {
+	return &PHPExtensionsService{}
+}
+
+func (p PHPExtensionsService) Page(req request.PHPExtensionsSearch) (int64, []response.PHPExtensionsDTO, error) {
+	var (
+		total      int64
+		extensions []model.PHPExtensions
+		err        error
+		result     []response.PHPExtensionsDTO
+	)
+	total, extensions, err = phpExtensionsRepo.Page(req.Page, req.PageSize)
+	if err != nil {
+		return 0, nil, err
+	}
+	for _, extension := range extensions {
+		result = append(result, response.PHPExtensionsDTO{
+			PHPExtensions: extension,
+		})
+	}
+	return total, result, nil
+}
+
+func (p PHPExtensionsService) List() ([]response.PHPExtensionsDTO, error) {
+	var (
+		extensions []model.PHPExtensions
+		err        error
+		result     []response.PHPExtensionsDTO
+	)
+	extensions, err = phpExtensionsRepo.List()
+	if err != nil {
+		return nil, err
+	}
+	for _, extension := range extensions {
+		result = append(result, response.PHPExtensionsDTO{
+			PHPExtensions: extension,
+		})
+	}
+	return result, nil
+}
+
+func (p PHPExtensionsService) Create(req request.PHPExtensionsCreate) error {
+	exist, _ := phpExtensionsRepo.GetFirst(commonRepo.WithByName(req.Name))
+	if exist.ID == 0 {
+		return buserr.New(constant.ErrNameIsExist)
+	}
+	extension := model.PHPExtensions{
+		Name:       req.Name,
+		Extensions: req.Extensions,
+	}
+	return phpExtensionsRepo.Create(&extension)
+}
+
+func (p PHPExtensionsService) Update(req request.PHPExtensionsUpdate) error {
+	exist, err := phpExtensionsRepo.GetFirst(commonRepo.WithByID(req.ID))
+	if err != nil {
+		return err
+	}
+	exist.Extensions = req.Extensions
+	return phpExtensionsRepo.Save(&exist)
+}
+
+func (p PHPExtensionsService) Delete(req request.PHPExtensionsDelete) error {
+	return phpExtensionsRepo.DeleteBy(commonRepo.WithByID(req.ID))
+}
diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go
index 8380cad7f..aaf2d1647 100644
--- a/backend/init/migration/migrate.go
+++ b/backend/init/migration/migrate.go
@@ -62,6 +62,8 @@ func Init() {
 		migrations.AddDefaultCA,
 		migrations.AddSettingRecycleBin,
 		migrations.UpdateWebsiteBackupRecord,
+
+		migrations.AddTablePHPExtensions,
 	})
 	if err := m.Migrate(); err != nil {
 		global.LOG.Error(err)
diff --git a/backend/init/migration/migrations/v_1_9.go b/backend/init/migration/migrations/v_1_9.go
index 2c212b621..cba434b6b 100644
--- a/backend/init/migration/migrations/v_1_9.go
+++ b/backend/init/migration/migrations/v_1_9.go
@@ -103,3 +103,16 @@ var UpdateWebsiteBackupRecord = &gormigrate.Migration{
 		return nil
 	},
 }
+
+var AddTablePHPExtensions = &gormigrate.Migration{
+	ID: "20240102-add-php-extensions",
+	Migrate: func(tx *gorm.DB) error {
+		if err := tx.AutoMigrate(&model.PHPExtensions{}); err != nil {
+			return err
+		}
+		if err := tx.Create(&model.PHPExtensions{Name: "默认", Extensions: "bcmath,gd,gettext,intl,pcntl,shmop,soap,sockets,sysvsem,xmlrpc,zip"}).Error; err != nil {
+			return err
+		}
+		return nil
+	},
+}
diff --git a/backend/router/ro_runtime.go b/backend/router/ro_runtime.go
index c43a3c170..e77ada55b 100644
--- a/backend/router/ro_runtime.go
+++ b/backend/router/ro_runtime.go
@@ -20,10 +20,16 @@ func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) {
 		groupRouter.POST("/del", baseApi.DeleteRuntime)
 		groupRouter.POST("/update", baseApi.UpdateRuntime)
 		groupRouter.GET("/:id", baseApi.GetRuntime)
+
 		groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript)
 		groupRouter.POST("/operate", baseApi.OperateRuntime)
 		groupRouter.POST("/node/modules", baseApi.GetNodeModules)
 		groupRouter.POST("/node/modules/operate", baseApi.OperateNodeModules)
+
+		groupRouter.POST("/php/extensions/search", baseApi.PagePHPExtensions)
+		groupRouter.POST("/php/extensions", baseApi.CreatePHPExtensions)
+		groupRouter.POST("/php/extensions/update", baseApi.UpdatePHPExtensions)
+		groupRouter.POST("/php/extensions/del", baseApi.DeletePHPExtensions)
 	}
 
 }
diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go
index cc84b0314..7f9a3d3e3 100644
--- a/cmd/server/docs/docs.go
+++ b/cmd/server/docs/docs.go
@@ -8995,6 +8995,144 @@ const docTemplate = `{
                 }
             }
         },
+        "/runtimes/php/extensions": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Create Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Create Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsCreate"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                }
+            }
+        },
+        "/runtimes/php/extensions/del": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Delete Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Delete Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsDelete"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                }
+            }
+        },
+        "/runtimes/php/extensions/search": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Page Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Page Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsSearch"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/definitions/response.PHPExtensionsDTO"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/runtimes/php/extensions/update": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Update Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Update Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsUpdate"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                }
+            }
+        },
         "/runtimes/search": {
             "post": {
                 "security": [
@@ -19390,6 +19528,65 @@ const docTemplate = `{
                 }
             }
         },
+        "request.PHPExtensionsCreate": {
+            "type": "object",
+            "required": [
+                "extensions",
+                "name"
+            ],
+            "properties": {
+                "extensions": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                }
+            }
+        },
+        "request.PHPExtensionsDelete": {
+            "type": "object",
+            "required": [
+                "id"
+            ],
+            "properties": {
+                "id": {
+                    "type": "integer"
+                }
+            }
+        },
+        "request.PHPExtensionsSearch": {
+            "type": "object",
+            "required": [
+                "page",
+                "pageSize"
+            ],
+            "properties": {
+                "all": {
+                    "type": "boolean"
+                },
+                "page": {
+                    "type": "integer"
+                },
+                "pageSize": {
+                    "type": "integer"
+                }
+            }
+        },
+        "request.PHPExtensionsUpdate": {
+            "type": "object",
+            "required": [
+                "extensions",
+                "id"
+            ],
+            "properties": {
+                "extensions": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "integer"
+                }
+            }
+        },
         "request.PortUpdate": {
             "type": "object",
             "properties": {
@@ -20915,6 +21112,26 @@ const docTemplate = `{
                 }
             }
         },
+        "response.PHPExtensionsDTO": {
+            "type": "object",
+            "properties": {
+                "createdAt": {
+                    "type": "string"
+                },
+                "extensions": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "integer"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "updatedAt": {
+                    "type": "string"
+                }
+            }
+        },
         "response.WebsiteAcmeAccountDTO": {
             "type": "object",
             "properties": {
diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json
index 7a3fcbdf4..5b5a7929f 100644
--- a/cmd/server/docs/swagger.json
+++ b/cmd/server/docs/swagger.json
@@ -8988,6 +8988,144 @@
                 }
             }
         },
+        "/runtimes/php/extensions": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Create Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Create Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsCreate"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                }
+            }
+        },
+        "/runtimes/php/extensions/del": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Delete Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Delete Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsDelete"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                }
+            }
+        },
+        "/runtimes/php/extensions/search": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Page Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Page Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsSearch"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/definitions/response.PHPExtensionsDTO"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/runtimes/php/extensions/update": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "Update Extensions",
+                "consumes": [
+                    "application/json"
+                ],
+                "tags": [
+                    "PHP Extensions"
+                ],
+                "summary": "Update Extensions",
+                "parameters": [
+                    {
+                        "description": "request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/request.PHPExtensionsUpdate"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK"
+                    }
+                }
+            }
+        },
         "/runtimes/search": {
             "post": {
                 "security": [
@@ -19383,6 +19521,65 @@
                 }
             }
         },
+        "request.PHPExtensionsCreate": {
+            "type": "object",
+            "required": [
+                "extensions",
+                "name"
+            ],
+            "properties": {
+                "extensions": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                }
+            }
+        },
+        "request.PHPExtensionsDelete": {
+            "type": "object",
+            "required": [
+                "id"
+            ],
+            "properties": {
+                "id": {
+                    "type": "integer"
+                }
+            }
+        },
+        "request.PHPExtensionsSearch": {
+            "type": "object",
+            "required": [
+                "page",
+                "pageSize"
+            ],
+            "properties": {
+                "all": {
+                    "type": "boolean"
+                },
+                "page": {
+                    "type": "integer"
+                },
+                "pageSize": {
+                    "type": "integer"
+                }
+            }
+        },
+        "request.PHPExtensionsUpdate": {
+            "type": "object",
+            "required": [
+                "extensions",
+                "id"
+            ],
+            "properties": {
+                "extensions": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "integer"
+                }
+            }
+        },
         "request.PortUpdate": {
             "type": "object",
             "properties": {
@@ -20908,6 +21105,26 @@
                 }
             }
         },
+        "response.PHPExtensionsDTO": {
+            "type": "object",
+            "properties": {
+                "createdAt": {
+                    "type": "string"
+                },
+                "extensions": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "integer"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "updatedAt": {
+                    "type": "string"
+                }
+            }
+        },
         "response.WebsiteAcmeAccountDTO": {
             "type": "object",
             "properties": {
diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml
index bc20e3b66..a0d93370f 100644
--- a/cmd/server/docs/swagger.yaml
+++ b/cmd/server/docs/swagger.yaml
@@ -3706,6 +3706,45 @@ definitions:
       codeDir:
         type: string
     type: object
+  request.PHPExtensionsCreate:
+    properties:
+      extensions:
+        type: string
+      name:
+        type: string
+    required:
+    - extensions
+    - name
+    type: object
+  request.PHPExtensionsDelete:
+    properties:
+      id:
+        type: integer
+    required:
+    - id
+    type: object
+  request.PHPExtensionsSearch:
+    properties:
+      all:
+        type: boolean
+      page:
+        type: integer
+      pageSize:
+        type: integer
+    required:
+    - page
+    - pageSize
+    type: object
+  request.PHPExtensionsUpdate:
+    properties:
+      extensions:
+        type: string
+      id:
+        type: integer
+    required:
+    - extensions
+    - id
+    type: object
   request.PortUpdate:
     properties:
       key:
@@ -4732,6 +4771,19 @@ definitions:
       uploadMaxSize:
         type: string
     type: object
+  response.PHPExtensionsDTO:
+    properties:
+      createdAt:
+        type: string
+      extensions:
+        type: string
+      id:
+        type: integer
+      name:
+        type: string
+      updatedAt:
+        type: string
+    type: object
   response.WebsiteAcmeAccountDTO:
     properties:
       createdAt:
@@ -10612,6 +10664,90 @@ paths:
         formatEN: Operate runtime [name]
         formatZH: 操作运行环境 [name]
         paramKeys: []
+  /runtimes/php/extensions:
+    post:
+      consumes:
+      - application/json
+      description: Create Extensions
+      parameters:
+      - description: request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/request.PHPExtensionsCreate'
+      responses:
+        "200":
+          description: OK
+      security:
+      - ApiKeyAuth: []
+      summary: Create Extensions
+      tags:
+      - PHP Extensions
+  /runtimes/php/extensions/del:
+    post:
+      consumes:
+      - application/json
+      description: Delete Extensions
+      parameters:
+      - description: request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/request.PHPExtensionsDelete'
+      responses:
+        "200":
+          description: OK
+      security:
+      - ApiKeyAuth: []
+      summary: Delete Extensions
+      tags:
+      - PHP Extensions
+  /runtimes/php/extensions/search:
+    post:
+      consumes:
+      - application/json
+      description: Page Extensions
+      parameters:
+      - description: request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/request.PHPExtensionsSearch'
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/response.PHPExtensionsDTO'
+            type: array
+      security:
+      - ApiKeyAuth: []
+      summary: Page Extensions
+      tags:
+      - PHP Extensions
+  /runtimes/php/extensions/update:
+    post:
+      consumes:
+      - application/json
+      description: Update Extensions
+      parameters:
+      - description: request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/request.PHPExtensionsUpdate'
+      responses:
+        "200":
+          description: OK
+      security:
+      - ApiKeyAuth: []
+      summary: Update Extensions
+      tags:
+      - PHP Extensions
   /runtimes/search:
     post:
       consumes:
diff --git a/frontend/src/api/interface/runtime.ts b/frontend/src/api/interface/runtime.ts
index 8fc913ecb..228b219d5 100644
--- a/frontend/src/api/interface/runtime.ts
+++ b/frontend/src/api/interface/runtime.ts
@@ -97,4 +97,29 @@ export namespace Runtime {
         Module?: string;
         PkgManager?: string;
     }
+
+    export interface PHPExtensions extends CommonModel {
+        id: number;
+        name: string;
+        extensions: string;
+    }
+
+    export interface PHPExtensionsList extends ReqPage {
+        all: boolean;
+    }
+
+    export interface PHPExtensionsCreate {
+        name: string;
+        extensions: string;
+    }
+
+    export interface PHPExtensionsUpdate {
+        id: number;
+        name: string;
+        extensions: string;
+    }
+
+    export interface PHPExtensionsDelete {
+        id: number;
+    }
 }
diff --git a/frontend/src/api/modules/runtime.ts b/frontend/src/api/modules/runtime.ts
index d09bfff2a..8562b6692 100644
--- a/frontend/src/api/modules/runtime.ts
+++ b/frontend/src/api/modules/runtime.ts
@@ -1,5 +1,5 @@
 import http from '@/api';
-import { ResPage } from '../interface';
+import { ResPage, ReqPage } from '../interface';
 import { Runtime } from '../interface/runtime';
 import { TimeoutEnum } from '@/enums/http-enum';
 
@@ -38,3 +38,23 @@ export const GetNodeModules = (req: Runtime.NodeModuleReq) => {
 export const OperateNodeModule = (req: Runtime.NodeModuleReq) => {
     return http.post<any>(`/runtimes/node/modules/operate`, req, TimeoutEnum.T_10M);
 };
+
+export const SearchPHPExtensions = (req: ReqPage) => {
+    return http.post<ResPage<Runtime.PHPExtensions>>(`/runtimes/php/extensions/search`, req);
+};
+
+export const ListPHPExtensions = (req: Runtime.PHPExtensionsList) => {
+    return http.post<Runtime.PHPExtensions[]>(`/runtimes/php/extensions/search`, req);
+};
+
+export const CreatePHPExtensions = (req: Runtime.PHPExtensionsCreate) => {
+    return http.post<any>(`/runtimes/php/extensions`, req);
+};
+
+export const UpdatePHPExtensions = (req: Runtime.PHPExtensionsUpdate) => {
+    return http.post<any>(`/runtimes/php/extensions/update`, req);
+};
+
+export const DeletePHPExtensions = (req: Runtime.PHPExtensionsDelete) => {
+    return http.post<any>(`/runtimes/php/extensions/del`, req);
+};
diff --git a/frontend/src/global/form-rules.ts b/frontend/src/global/form-rules.ts
index 6c3b58c44..cae019b0d 100644
--- a/frontend/src/global/form-rules.ts
+++ b/frontend/src/global/form-rules.ts
@@ -474,6 +474,19 @@ const checkFilePermission = (rule, value, callback) => {
     }
 };
 
+const checkPHPExtensions = (rule, value, callback) => {
+    if (value === '' || typeof value === 'undefined' || value == null) {
+        callback(new Error(i18n.global.t('commons.rule.phpExtension')));
+    } else {
+        const reg = /^[a-z0-9,_]{3,300}$/;
+        if (!reg.test(value)) {
+            callback(new Error(i18n.global.t('commons.rule.phpExtension')));
+        } else {
+            callback();
+        }
+    }
+};
+
 interface CommonRule {
     requiredInput: FormItemRule;
     requiredSelect: FormItemRule;
@@ -507,6 +520,7 @@ interface CommonRule {
     leechExts: FormItemRule;
     domainWithPort: FormItemRule;
     filePermission: FormItemRule;
+    phpExtensions: FormItemRule;
 
     paramCommon: FormItemRule;
     paramComplexity: FormItemRule;
@@ -711,4 +725,9 @@ export const Rules: CommonRule = {
         validator: checkFilePermission,
         trigger: 'blur',
     },
+    phpExtensions: {
+        required: true,
+        validator: checkPHPExtensions,
+        trigger: 'blur',
+    },
 };
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index 9ccc67588..471bb81db 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -191,6 +191,7 @@ const message = {
             paramSimple: 'Support lowercase letters and numbers, length 1-128',
             filePermission: 'File Permission Error',
             formatErr: 'Format error, please check and retry',
+            phpExtension: 'Only supports , _ lowercase English and numbers',
         },
         res: {
             paramError: 'The request failed, please try again later!',
@@ -1839,6 +1840,10 @@ const message = {
         uploadMaxSize: 'Upload limit',
         indexHelper:
             'In order to ensure the normal operation of the PHP website, please place the code in the index directory and avoid renaming',
+        extensions: 'Extension template',
+        extension: 'Extension',
+        extensionHelper: 'Please use multiple extensions, split',
+        toExtensionsList: 'View extension list',
     },
     nginx: {
         serverNamesHashBucketSizeHelper: 'The hash table size of the server name',
diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts
index 8a8f0d93c..c389a6656 100644
--- a/frontend/src/lang/modules/tw.ts
+++ b/frontend/src/lang/modules/tw.ts
@@ -190,6 +190,7 @@ const message = {
             paramSimple: '支持小寫字母和數字,長度 1-128',
             filePermission: '權限錯誤',
             formatErr: '格式錯誤,檢查後重試',
+            phpExtension: '僅支持 , _ 小寫英文和數字',
         },
         res: {
             paramError: '請求失敗,請稍後重試!',
@@ -1725,6 +1726,10 @@ const message = {
         disableFunctionHelper: '輸入要禁用的函數,例如exec,多個請用,分割',
         uploadMaxSize: '上傳限製',
         indexHelper: '為保障PHP網站正常運行,請將代碼放置於 index 目錄,並避免重命名',
+        extensions: '擴充範本',
+        extension: '擴充',
+        extensionHelper: '多個擴充功能,分割',
+        toExtensionsList: '檢視擴充清單',
     },
     nginx: {
         serverNamesHashBucketSizeHelper: '服務器名字的hash表大小',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index 32ca4d837..f419b3ae2 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -190,6 +190,7 @@ const message = {
             paramSimple: '支持小写字母和数字,长度1-128',
             filePermission: '权限错误',
             formatErr: '格式错误,检查后重试',
+            phpExtension: '仅支持 , _ 小写英文和数字',
         },
         res: {
             paramError: '请求失败,请稍后重试!',
@@ -1725,6 +1726,10 @@ const message = {
         disableFunctionHelper: '输入要禁用的函数,例如exec,多个请用,分割',
         uploadMaxSize: '上传限制',
         indexHelper: '为保障 PHP 网站正常运行,请将代码放置于主目录下的 index 目录,并避免重命名',
+        extensions: '扩展模版',
+        extension: '扩展',
+        extensionsHelper: '多个扩展请用,分割',
+        toExtensionsList: '查看扩展列表',
     },
     nginx: {
         serverNamesHashBucketSizeHelper: '服务器名字的hash表大小',
diff --git a/frontend/src/views/website/runtime/php/create/index.vue b/frontend/src/views/website/runtime/php/create/index.vue
index 75e967fa3..bb3e9e2b8 100644
--- a/frontend/src/views/website/runtime/php/create/index.vue
+++ b/frontend/src/views/website/runtime/php/create/index.vue
@@ -81,7 +81,16 @@
                                         {{ $t('runtime.phpsourceHelper') }}
                                     </span>
                                 </el-form-item>
-
+                                <el-form-item :label="$t('php.extensions')">
+                                    <el-select v-model="extensions" @change="changePHPExtension()">
+                                        <el-option
+                                            v-for="(extension, index) in phpExtensions"
+                                            :key="index"
+                                            :label="extension.name"
+                                            :value="extension.extensions"
+                                        ></el-option>
+                                    </el-select>
+                                </el-form-item>
                                 <Params
                                     v-if="mode === 'create'"
                                     v-model:form="runtime.params"
@@ -146,7 +155,7 @@
 import { App } from '@/api/interface/app';
 import { Runtime } from '@/api/interface/runtime';
 import { GetApp, GetAppDetail, SearchApp } from '@/api/modules/app';
-import { CreateRuntime, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
+import { CreateRuntime, GetRuntime, ListPHPExtensions, UpdateRuntime } from '@/api/modules/runtime';
 import { Rules } from '@/global/form-rules';
 import i18n from '@/lang';
 import { MsgSuccess } from '@/utils/message';
@@ -172,6 +181,7 @@ const mode = ref('create');
 const appParams = ref<App.AppParams>();
 const editParams = ref<App.InstallParams[]>();
 const appVersions = ref<string[]>([]);
+const phpExtensions = ref([]);
 const appReq = reactive({
     type: 'php',
     page: 1,
@@ -187,6 +197,7 @@ const initData = (type: string) => ({
     rebuild: false,
     source: 'mirrors.ustc.edu.cn',
 });
+const extensions = ref();
 
 let runtime = reactive<Runtime.RuntimeCreate>(initData('php'));
 
@@ -364,6 +375,21 @@ const getRuntime = async (id: number) => {
     } catch (error) {}
 };
 
+const listPHPExtensions = async () => {
+    try {
+        const res = await ListPHPExtensions({
+            all: true,
+            page: 1,
+            pageSize: 100,
+        });
+        phpExtensions.value = res.data;
+    } catch (error) {}
+};
+
+const changePHPExtension = () => {
+    runtime.params['PHP_EXTENSIONS'] = extensions.value.split(',');
+};
+
 const acceptParams = async (props: OperateRrops) => {
     mode.value = props.mode;
     initParam.value = false;
@@ -374,6 +400,8 @@ const acceptParams = async (props: OperateRrops) => {
         searchApp(props.appID);
         getRuntime(props.id);
     }
+    extensions.value = '';
+    listPHPExtensions();
     open.value = true;
 };
 
diff --git a/frontend/src/views/website/runtime/php/extensions/index.vue b/frontend/src/views/website/runtime/php/extensions/index.vue
new file mode 100644
index 000000000..278cc0c69
--- /dev/null
+++ b/frontend/src/views/website/runtime/php/extensions/index.vue
@@ -0,0 +1,101 @@
+<template>
+    <el-drawer :close-on-click-modal="false" v-model="open" size="50%" :before-close="handleClose">
+        <template #header>
+            <DrawerHeader :header="$t('php.extensions')" :back="handleClose" />
+        </template>
+        <ComplexTable :data="data" @search="search()" :pagination-config="paginationConfig">
+            <template #toolbar>
+                <el-button type="primary" @click="openCreate">{{ $t('commons.button.create') }}</el-button>
+            </template>
+            <el-table-column :label="$t('commons.table.name')" width="150px" prop="name"></el-table-column>
+            <el-table-column :label="$t('php.extension')" fix prop="extensions"></el-table-column>
+            <fu-table-operations
+                :ellipsis="10"
+                width="120px"
+                :buttons="buttons"
+                :label="$t('commons.table.operate')"
+                fixed="right"
+                fix
+            />
+        </ComplexTable>
+        <Create ref="createRef" @close="search()" />
+        <OpDialog ref="opRef" @search="search" />
+    </el-drawer>
+</template>
+<script lang="ts" setup>
+import { DeletePHPExtensions, SearchPHPExtensions } from '@/api/modules/runtime';
+import { reactive, ref } from 'vue';
+import Create from './operate/index.vue';
+import { Runtime } from '@/api/interface/runtime';
+import i18n from '@/lang';
+
+const open = ref(false);
+const data = ref();
+const createRef = ref();
+const opRef = ref();
+
+const paginationConfig = reactive({
+    cacheSizeKey: 'website-page-size',
+    currentPage: 1,
+    pageSize: Number(localStorage.getItem('website-page-size')) || 10,
+    total: 0,
+});
+
+const buttons = [
+    {
+        label: i18n.global.t('commons.button.edit'),
+        click: function (row: Runtime.PHPExtensions) {
+            openUpdate(row);
+        },
+    },
+    {
+        label: i18n.global.t('commons.button.delete'),
+        click: function (row: Runtime.PHPExtensions) {
+            openDelete(row);
+        },
+    },
+];
+
+const handleClose = () => {
+    open.value = false;
+};
+
+const acceptParams = (): void => {
+    open.value = true;
+    search();
+};
+
+const openCreate = () => {
+    createRef.value.acceptParams('create');
+};
+
+const search = async () => {
+    try {
+        const res = await SearchPHPExtensions({
+            page: paginationConfig.currentPage,
+            pageSize: paginationConfig.pageSize,
+        });
+        data.value = res.data.items;
+        paginationConfig.total = res.data.total;
+    } catch (error) {}
+};
+
+const openDelete = async (row: Runtime.PHPExtensions) => {
+    opRef.value.acceptParams({
+        title: i18n.global.t('commons.msg.deleteTitle'),
+        names: [row.name],
+        msg: i18n.global.t('commons.msg.operatorHelper', [
+            i18n.global.t('php.extensions'),
+            i18n.global.t('commons.button.delete'),
+        ]),
+        api: DeletePHPExtensions,
+        params: { id: row.id },
+    });
+};
+
+const openUpdate = async (row: Runtime.PHPExtensions) => {
+    createRef.value.acceptParams('edit', row);
+};
+
+defineExpose({ acceptParams });
+</script>
diff --git a/frontend/src/views/website/runtime/php/extensions/operate/index.vue b/frontend/src/views/website/runtime/php/extensions/operate/index.vue
new file mode 100644
index 000000000..b451fe613
--- /dev/null
+++ b/frontend/src/views/website/runtime/php/extensions/operate/index.vue
@@ -0,0 +1,121 @@
+<template>
+    <el-dialog
+        v-model="open"
+        :title="$t('commons.button.' + operate) + $t('php.extensions')"
+        :close-on-click-modal="false"
+        width="30%"
+        :before-close="handleClose"
+    >
+        <el-row v-loading="loading">
+            <el-col :span="22" :offset="1">
+                <el-form @submit.prevent ref="extensionsForm" label-position="top" :model="extensions" :rules="rules">
+                    <el-form-item :label="$t('commons.table.name')" prop="name">
+                        <el-input v-model.trim="extensions.name" :disabled="operate == 'edit'"></el-input>
+                    </el-form-item>
+                    <el-form-item :label="$t('php.extension')" prop="extensions">
+                        <el-input
+                            type="textarea"
+                            :placeholder="$t('php.extensionsHelper')"
+                            :autosize="{ minRows: 3, maxRows: 10 }"
+                            v-model="extensions.extensions"
+                        />
+                    </el-form-item>
+                    <a target="“_blank”" href="https://1panel.cn/docs/user_manual/websites/php/#php_1">
+                        {{ $t('php.toExtensionsList') }}
+                    </a>
+                </el-form>
+            </el-col>
+        </el-row>
+        <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="handleClose" :disabled="loading">
+                    {{ $t('commons.button.cancel') }}
+                </el-button>
+                <el-button type="primary" @click="submit(extensionsForm)" :disabled="loading">
+                    {{ $t('commons.button.confirm') }}
+                </el-button>
+            </span>
+        </template>
+    </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import { Rules } from '@/global/form-rules';
+import { FormInstance } from 'element-plus';
+import { MsgSuccess } from '@/utils/message';
+import { CreatePHPExtensions, UpdatePHPExtensions } from '@/api/modules/runtime';
+import i18n from '@/lang';
+import { Runtime } from '@/api/interface/runtime';
+
+const open = ref(false);
+const operate = ref('create');
+const loading = ref(false);
+const updateID = ref(0);
+const extensionsForm = ref<FormInstance>();
+const rules = ref({
+    name: [Rules.requiredInput],
+    extensions: [Rules.requiredInput, Rules.phpExtensions],
+});
+const em = defineEmits(['close']);
+
+const initData = () => ({
+    name: '',
+    extensions: '',
+});
+
+const extensions = ref(initData());
+
+const acceptParams = (op: string, extend: Runtime.PHPExtensions) => {
+    operate.value = op;
+    open.value = true;
+    extensions.value = initData();
+    if (operate.value == 'edit') {
+        extensions.value = extend;
+        updateID.value = extend.id;
+    }
+};
+
+const handleClose = () => {
+    open.value = false;
+    extensionsForm.value?.resetFields();
+    em('close', false);
+};
+
+const submit = async (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    await formEl.validate((valid) => {
+        if (!valid) {
+            return;
+        }
+        loading.value = true;
+        if (operate.value == 'create') {
+            CreatePHPExtensions(extensions.value)
+                .then(() => {
+                    MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
+                    handleClose();
+                })
+                .finally(() => {
+                    loading.value = false;
+                });
+        } else {
+            UpdatePHPExtensions({
+                id: updateID.value,
+                name: extensions.value.name,
+                extensions: extensions.value.extensions,
+            })
+                .then(() => {
+                    MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
+                    handleClose();
+                })
+                .finally(() => {
+                    loading.value = false;
+                });
+        }
+    });
+};
+
+defineExpose({
+    acceptParams,
+});
+</script>
diff --git a/frontend/src/views/website/runtime/php/index.vue b/frontend/src/views/website/runtime/php/index.vue
index 9b7b23a76..bcbb86717 100644
--- a/frontend/src/views/website/runtime/php/index.vue
+++ b/frontend/src/views/website/runtime/php/index.vue
@@ -13,6 +13,10 @@
                 <el-button type="primary" @click="openCreate">
                     {{ $t('runtime.create') }}
                 </el-button>
+
+                <el-button @click="openExtensions">
+                    {{ $t('php.extensions') }}
+                </el-button>
             </template>
             <template #main>
                 <ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
@@ -74,6 +78,7 @@
         <CreateRuntime ref="createRef" @close="search" @submit="openCreateLog" />
         <OpDialog ref="opRef" @search="search" />
         <Log ref="logRef" @close="search" />
+        <Extensions ref="extensionsRef" @close="search" />
     </div>
 </template>
 
@@ -88,6 +93,7 @@ import Status from '@/components/status/index.vue';
 import i18n from '@/lang';
 import RouterMenu from '../index.vue';
 import Log from '@/components/log-dialog/index.vue';
+import Extensions from './extensions/index.vue';
 
 const paginationConfig = reactive({
     cacheSizeKey: 'runtime-page-size',
@@ -104,6 +110,7 @@ let req = reactive<Runtime.RuntimeReq>({
 let timer: NodeJS.Timer | null = null;
 const opRef = ref();
 const logRef = ref();
+const extensionsRef = ref();
 
 const buttons = [
     {
@@ -156,13 +163,17 @@ const openCreateLog = (id: number) => {
     logRef.value.acceptParams({ id: id, type: 'php', tail: true });
 };
 
+const openExtensions = () => {
+    extensionsRef.value.acceptParams();
+};
+
 const openDelete = async (row: Runtime.Runtime) => {
     opRef.value.acceptParams({
         title: i18n.global.t('commons.msg.deleteTitle'),
         names: [row.name],
         msg: i18n.global.t('commons.msg.operatorHelper', [
             i18n.global.t('website.runtime'),
-            i18n.global.t('commons.msg.delete'),
+            i18n.global.t('commons.button.delete'),
         ]),
         api: DeleteRuntime,
         params: { id: row.id, forceDelete: true },